Concurrency in Go: the Sync package
Goroutines, the primary concept on which Go’s concurrency model is built, would only help us with running tasks in parallel. But more often than not, Goroutines alone is not sufficient. We often end up in scenarios where several parallel running tasks are dependent on each other. This dependency is expressed in two folds -
- Sequencing — One Goroutine waiting for another Goroutine to finish execution.
- Memory sharing — One Goroutine accessing the data written by another or vice-versa.
In Go, the Sync package provides this dependency management ie. the mechanism necessary for co-ordination and memory sharing among the Goroutines. Main actors in this package are -
- WaitGroups,
- Mutex locks
We shall look at both of them in detail, one by one below —
WaitGroup
A WaitGroup waits for a set of Goroutines to finish. Let us say we want a Goroutine to end only after the completion of two Goroutines. For this, we can initialize a WaitGroup
and add the subsequent running Goroutines to this group. When these Goroutines are done, they’d call the done()
method on the WaitGroup
. In the meantime, the main Goroutine that is waiting on these Goroutines to finish will be blocked until it receives a signal. This is achieved by calling wait()
on the WaitGroup
struct.
Let us consider the following scenario — We have a web service exposing two Apis to store and retrieve data from a database respectively. While retrieving this data from GET API, we need lesser latency and so, we want the POST API to insert data in an external cache as well. This means that POST API will return success only after data is written into both the database and the supporting cache.
To satisfy the latency requirement, we can parallelize the writes to the two different sources by using two Goroutines, one for cache and the other for the database. But how do we know when to respond to the user with the completion of the request? This is where we introduce a WaitGroup
. Our main thread would initialize a waitGroup
and sends it to the two Goroutines. Below is a snippet illustrating the concept —
Mutex locks
Similar to the other languages, Go also supports the locking mechanism through the Mutex data structure. Mutual + exclusion => Mutex. In Sync package, Mutex is defined as follows —
type Mutex struct {/**
internal data----
*.// to restrict the access
Lock()// to allow the acceess
UnLock()}
Locks are needed to prevent the access of data by multiple Goroutines at the same time. Consider an Airline management system where there are two available seats for a specific Airplane on a day. Let us say that there are two simultaneous requests to book seats. With the code below,
availableSeats = 2func bookSeat() {// implementation for payment// payment successavailableSeats = availableSeats-1}
If two Goroutines run at the same time, they both would update the availbleSeats
to 1, which is wrong as two seats are already booked by both of them. This is why we need a synchronous way of accessing shared data and that is exactly where we are going to use locks.
When a Goroutine calls lock()
access to subsequent code until unlock()
, (known as a critical section ) is restricted by only a single Goroutine. If some other Goroutine has already called lock()
, this Goroutine waits until an unlock()
is called. Let us see the syntax and the usage with the below code snippet —
And following is the output —
Without the locking mechanism, availableSeats
could have been either 0 or 1 based on the manner OS schedules the Goroutines.
Mutex locks give us the capability of mutual exclusion of a resource, among the concurrently running Goroutines. They can also be used in cases where one Goroutine writes some data into a resource and another Goroutine waits until the write is performed and then starts reading. We can use Read-Write locks( another variant of Mutex) to restrict the access to the resource. But, this can be achieved in an efficient manner through Channels
. Channels will be covered in the coming article under this series about Concurrency in Go.
Wanna play around with the WaitGroups and Locks? try here — https://play.golang.org/