

A closer look at go sync package
source link: https://medium.com/@teivah/a-closer-look-at-go-sync-package-9f4e4a28c35a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Let’s take a look at the Go package in charge to provide synchronization primitives: sync
.
sync.Mutex
sync.Mutex
is probably the most widely used primitive of the sync
package. It allows a mutual exclusion on a shared resource (no simultaneous access):
It must be pointed out that a sync.Mutex
cannot be copied (just like all the other primitives of sync
package).
In the following example, the foo
receiver is a value and not a pointer:
This is a bug as the slice is never modified safely. If a structure has a sync
field, it must be passed by pointer.
sync.RWMutex
sync.RWMutex
is a reader/writer mutex. It provides the same methods that we have just seen Lock()
/ Unlock()
(as both structures implement the sync.Locker
interface).
In addition, we also have RLock()
and RUnlock()
:
RLock()
and RUnlock
must only be used by goroutines reading a shared variable. On the other side, Lock()
and Unlock()
should only be used by goroutines modifying a shared variable.
When should we favor sync.Mutex
or sync.RWMutex
? Let’s run three benchmarks:
- Benchmark_Mutex_LockUnlock : using
sync.Mutex
andLock()
/Unlock()
- Benchmark_RWMutex_LockUnlock : using
sync.RWMutex
andLock()
/Unlock()
- Benchmark_RWMutex_RLockRUnlock : using
sync.RWMutex
andRLock()
/RUnlock()
Benchmark_Mutex_LockUnlock-4 74900307 13.7 ns/op Benchmark_RWMutex_LockUnlock-4 42814406 32.4 ns/op Benchmark_RWMutex_RLockRUnlock-4 93391347 12.5 ns/op
As we can notice, read locking/unlocking a sync.RWMutex
is faster than locking/unlocking a sync.Mutex
. On the other end, calling Lock()
/ Unlock()
on a sync.RWMutex
is the slowest benchmark.
In conclusion, a sync.RWMutex
should be used when we have frequent reads and infrequent writes .
sync.WaitGroup
sync.WaitGroup
tends also to be used quite frequently. It’s the idiomatic way for a goroutine to wait for the completion of a collection of goroutines .
sync.WaitGroup
holds an internal counter. If this counter is equal to 0, the Wait()
method returns immediately. Otherwise, it is blocked until the counter is 0.
To increment the counter we have to use Add(int)
. To decrement it we can either use Done()
(that will decrement by 1) or the same Add(int)
method with a negative value.
In the following example, we will spin up 8 goroutines and wait for the completion of all of them:
Each time we create a goroutine, we increment the wg
‘s internal counter with wg.Add(1)
. We could have also called wg.Add(8)
outside of the for-loop.
Meanwhile, every time a goroutine completes, it decreases the wg
‘s internal counter using wg.Done()
.
The main goroutine continues its execution once the 8 wg.Done()
statements have been executed.
sync.Map
sync.Map
is a concurrent version of Go map
where we can:
Store(interface{}, interface{}) Load(interface) interface{} Delete(interface{}) LoadOrStore(interface{}, interface{}) (interface, bool) Range
one three 1: one 2: two
As you can see, the Range
method takes a func(key, value interface{}) bool
function. If we return false, the iteration is stopped (interesting fact, for some reason the worst-case time-complexity remains O(n) even if we return false after a constant time).
When shall we use sync.Map
instead of a sync.Mutex
on top of a classic map
? According to the official documentation:
- When we have frequent reads and infrequent writes (in the same vein to
sync.RWMutex
) - When multiple goroutines read, write, and overwrite entries for disjoint sets of keys . What does it mean concretely? For example, if we have a sharding implementation with a set of 4 goroutines and each goroutine in charge of 25% of the keys ( without collision ). In this case,
sync.Map
is the preferred choice.
sync.Pool
sync.Pool
is a concurrent pool, in charge to hold safely a set of objects .
The public methods are:
Get() interface{} Put(interface{})
It worth noting that there is no guarantee in terms of ordering. The Get
method specifies that it takes an arbitrary item from the pool.
It is also possible to specify a creator method:
Every time Get()
is called, it will return an object created by the function passed in pool.New
.
When shall we use sync.Pool
? There are two use-cases.
The first one is when we have to reuse shared and long-live objects like a DB connection for example.
The second one is to optimize memory allocation .
Let’s consider the example of a function that writes into a buffer and persists the result in a file. With sync.Pool
, we can reuse the space allocated for the buffer by reusing the same object across the different function calls:
The first step is to retrieve the buffer previously allocated (or to create one if it’s the first call). Then, the deferred action is to put the buffer back in the pool.
This way, we can efficiently reuse the allocated memory as well as relieving the garbage collector if the variable was escaped to the heap.
sync.Once
sync.Once
is a simple and powerful primitive to guarantee that a function is executed only once.
In this example, there will be only one goroutine displaying the output message:
We have used the Do(func())
method to specify the part that must be called only once.
sync.Cond
Let’s finish by the primitive which is, most likely, the less frequently used: sync.Cond
.
It is used to emit a signal (one-to-one) or broadcast a signal (one-to-many) to goroutine(s).
Let’s consider a use-case where we have to emit a signal to one goroutine when the first element of a shared slice has been updated.
Creating a sync.Cond
requires a sync.Locker
object (either a sync.Mutex
or a sync.RWMutex
):
Then, let’s write the function in charge to display the first element of the slice:
As you can see, we can access the internal mutex using cond.L
. Once the lock is acquired, we call cond.Wait()
that is going to block as long as we receive a signal.
Let’s get back to the main goroutine. We’ll create a pool of printFirstElement
by referencing the shared slice and the sync.Cond
previously created. Then, we call a get()
function, store the result in s[0]
and emit a signal:
This signal will unblock one of the goroutine created that will display s[0]
.
Nevertheless, if we take a step back we could argue that our code might break one of the most fundamental principles of Go:
Do not communicate by sharing memory; instead, share memory by communicating.
Indeed, in this example, it would have been better to use a channel to communicate the value returned by get()
.
Yet, we also mentioned that sync.Cond
can also be used to broadcast a signal .
Let’s just modify the end of the previous example by calling Broadcast()
instead of Signal()
:
In this scenario, all of the goroutines are going to be triggered.
As channel elements are caught by only one goroutine, this is undeniably an interesting feature, despite being quite controversial .
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK