Classical Concurrency Patterns for Gophers

Sudaraka Jayathilaka
The Startup
Published in
4 min readJul 11, 2020
Image: https://caitiem.com/2016/08/18/a-quick-guide-to-testing-in-golang/

Concurrency is one of the most discussed topics is programming. A lot of concurrency patterns have been introduced over time in different programming languages. Golang is considered as a language with very rich support for concurrency. This article takes a deep dive into how these classical concurrency patterns can be adapted into Golang.

Go Concurrency Primitives

Golang’s rich support for concurrency is mainly based on two primitives

  • Goroutines
  • Channels

Two important principles about Go concurrency primitives,

  • Start goroutines when you have concurrent work
  • Share by communicating (not using shared variables, but with channels)

Let’s dive into the concurrency patterns.

Asynchronous APIs

In languages like JavaScript asynchronicity has been heavily used to handle concurrency. But concurrency is not asynchronicity as well. A code which is programmed to wait until an asynchronous operation finishes and move onto the next operation is not concurrent. Here are some common asynchronous patterns.

Futures

Futures are a common concurrent pattern where it returns a proxy object instead of returning the result. The proxy object will resolve into the response later. It's up to caller method to decide, whether to wait for the response or proceed with other tasks and use the response later. In Go, this pattern can be implemented using a single element buffered channel.

Producer-Consumer Queues

Producer and consumer pattern is a concurrency pattern which helps to increase the scalability of the code. Unlike the previous scenario, we will use an unbuffered channel and the producer can expect multiple responses depending on the situation. Here is a simple implementation.

Usually, these asynchronous patterns are used when we need to avoid method calls being blocked (eg: UI and network threads). We don’t get that advantage in Golang Since the Go runtime manages goroutines. But since goroutines can return from any of the stack frames, we get the advantage of reclaiming the stack frames efficiently. Meanwhile, the GC can claim the stack allocations which became unreachable since the particular goroutine returned. Since the asynchronous functions return immediately, the caller can proceed to make other calls.

But these asynchronous patterns comes with a cost as well. The caller side code becomes less clear to understand in most of the other languages. But in Golang, converting synchronous to asynchronous and vice versa is easy. So we don’t need to worry about the cons of asynchronous when using Golang in the implementation.

Condition Variables

Condition variables are another classic concurrency pattern. A more popular term for this pattern is monitors. This pattern has been in use for a long time now. We can easily implement this pattern using the Mutex in sync package in Golang. You can find the implementation in the below playground by Bryan C. Mills,

But this can be easily achieved using channels. It’s more elegant and clean compared to the mutex implementation. This implementation can be found in the following playground link by Bryan C. Mills.

There are multiple issues associated with the conditional variable same as other patterns. Let’s look into these issues with the following implementation,

type Queue struct {
items []Item
closed bool
mu sync.Mutex
itemAdded sync.Cond
}
func (q *Queue) Put(item Item) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.itemAdded.Broadcast()
}
func (q *Queue) GetMany(n int) []Item{
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items < n){
q.itemAdded.wait()
}
items = q.items[:n:n]
q.items = q.items[n:]
return items
}
  • If we broadcast (signalling to everyone who is waiting), it can result in waking up an unnecessary amount of waiters. although only one of them can actually complete.
  • If a GetMany call with n=300 and recurrentGetMany call with n = 3 is waiting, the call with n=300 can starve since every time Broadcast() is called, every worker gets woken up.
  • If a context gets cancelled while some goroutines are waiting, the waiting goroutines won’t be notified and the waiting will continue even after the context cancellation.

But using Golang channels will solve all of the above issues

c := make(chan Item)
// Putting something int the queue
c <- item
// Receiving something from the queue
receivedItem := <- c

Wrap Up

Golang is a programming language rich in concurrency features. It allows you to implement most of the common concurrency patterns easily and elegantly. This article was heavily inspired by a Gophercon talk Bryan C. Mills. You can find the talk in the following video.

--

--