51

A closer look at go sync package

 5 years ago
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.
neoserver,ios ssh client

rqEn6j2.jpg!web

This is not an analogy of the sync package quality :)

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 and Lock() / Unlock()
  • Benchmark_RWMutex_LockUnlock : using sync.RWMutex and Lock() / Unlock()
  • Benchmark_RWMutex_RLockRUnlock : using sync.RWMutex and RLock() / 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 .


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK