Concurrency Synchronization Techniques Provided In The sync Standard Package

The channel use cases article introduces many use cases where channels are used to do data synchronizations among goroutines. In fact, channels are not the only synchronization techniques provided in Go. There are some other synchronization techniques supported by Go. For some specific circumstances, using the synchronization techniques other than channel are more efficient and readable than using channels. Below will introduce the synchronization techniques provided in the sync standard package.

The sync standard package provides several types which can be used to do synchronizations for some specialized circumstances. and guarantee some specialized memory orders. For the specialized circumstances, these techniques are more efficient, and look cleaner, than the channel ways.

(Please note, to avoid abnormal behaviors, it is best never to copy the values of the types in the sync standard package.)

The sync.WaitGroup Type

Each sync.WaitGroup value maintains a counter internally. The default value of the counter is zero.

The *sync.WaitGroup type has three methods: Add(delta int), Done() and Wait().

For an addressable sync.WaitGroup value wg,

Please note that wg.Add(delta), wg.Done() and wg.Wait() are shorthands of (&wg).Add(delta), (&wg).Done() and (&wg).Wait(), respectively.

Generally, a sync.WaitGroup value is used for the scenario that one goroutine waits until all of several other goroutines finish their respective jobs. An example:
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano())

	const N = 5
	var values [N]int32
	
	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		i := i
		go func() {
			values[i] = 50 + rand.Int31n(50)
			fmt.Println("Done:", i)
			wg.Done() // <=> wg.Add(-1)
		}()
	}
	
	wg.Wait()
	// All elements are guaranteed to be initialized now.
	fmt.Println("values:", values)
}
In the above example, the main goroutine waits until all other N goroutines have populated their respective element value in values array. Here is one possible output result:
Done: 4
Done: 1
Done: 3
Done: 0
Done: 2
values: [71 89 50 62 60]
We can split the only Add method call in the above example into multiple ones.
...
	var wg sync.WaitGroup
	for i := 0; i < N; i++ {
		wg.Add(1) // will be invoked N times
		i := i
		go func() {
			values[i] = 50 + rand.Int31n(50)
			wg.Done()
		}()
	}
...
The Wait method can be called in multiple goroutines. When the counter becomes zero, all of them will be notified, in a broadcast way.
func() {
	var wg sync.WaitGroup
	wg.Add(1)

	for i := 0; i < N; i++ {
		i := i
		go func() {
			wg.Wait()
			fmt.Printf("values[%v]=%v \n", i, values[i])
		}()
	}

	// The loop is guaranteed to finish before
	// any above wg.Wait calls returns.
	for i := 0; i < N; i++ {
		values[i] = 50 + rand.Int31n(50)
	}
	wg.Done() // will make a broadcast
}

The sync.Once Type

A *sync.Once value has a Do(f func()) method, which takes a solo parameter with type func().

For an addressable sync.Once value o, the method o.Do, which is a shorthand of (&o).Do, can be called multiple times, in multiple goroutines. The arguments of these o.Do calls should (but not are required to) be the same function value.

Among these o.Do calls, only exact one argument function will be invoked. The invoked argument function is guaranteed to return before any o.Do method call returns. In other words, the code in the invoked argument function is guaranteed to be executed before any o.Do method call returns.

Generally, an sync.Once value is used to ensure that something has been done and only done once in concurrent programming.

An example:
package main

import (
	"log"
	"sync"
)

func main() {
	log.SetFlags(0)
	
	x := 0
	doSomething := func() {
		x++
		log.Println("Hello")
	}
	
	var wg sync.WaitGroup
	var once sync.Once
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(doSomething)
			log.Println("world!")
		}()
	}
	
	wg.Wait()
	log.Println("x =", x) // x = 1
}

In the above example, Hello will be printed once, but world! will be printed five times. And Hello is guaranteed to be printed before all five world!.

The sync.Mutex And sync.RWMutex Types

Both of the *sync.Mutex and *sync.RWMutex types implement the sync.Locker interface.

(1) Here the data reader should not be interpreted from literal. Some data readers may modify some data, but the modifications made by the data readers don't interference with each other.

A sync.Mutex value is a general mutual exclusion lock. A zero sync.Mutex value is an unlocked mutex. A sync.Mutex value can only be locked when it is in unblocked status. In other words, a data accessor using a sync.Mutex value excludes any other data accessors using the sync.Mutex value.

A sync.RWMutex value is a reader/writer mutual exclusion lock. A zero sync.RWMutex value an unlocked mutex. At the same time, a sync.RWMutex value can be locked by arbitrary number of data readers, but can only be locked by at most one general data accessor. In other words, Generally, An example of using sync.Mutex:
package main

import "fmt"
import "sync"

type Counter struct {
	m sync.RWMutex
	n uint64
}

func (c *Counter) Increase(delta uint64) {
	c.m.Lock()
	c.n += delta
	c.m.Unlock()
}

// The first two lines in this method can be replaced with:
//    c.m.RLock()
//    defer c.m.RUnlock()
func (c *Counter) Value() uint64 {
	c.m.Lock()
	defer c.m.Unlock()
	return c.n
}

func main() {
	var c Counter
	for i := 0; i < 100; i++ {
		go func() {
			for k := 0; k < 100; k++ {
				c.Increase(1)
			}
		}()
	}
	
	for c.Value() < 10000 {}
	fmt.Println(c.Value()) // 10000
}

In the above example, the Mutex field of any Counter value will guarantee that the n field of the Counter value will be never accessed by multiple goroutines parallelly.

An example of using sync.RWMutex:
package main

import (
	"fmt"
	"sync"
)

func main() {
	const N = 10
	var values [N]string

	var m sync.RWMutex
	for i := 0; i < N; i++ {
		i := i
		go func() {
			m.RLock()
			values[i] = string('a' + i)
			m.RUnlock()
		}()
	}

	done := func() bool {
		m.Lock()
		defer m.Unlock()
		for i := 0; i < N; i++ {
			if values[i] == "" {
				return false
			}
		}
		fmt.Println(values) // [a b c d e f g h i j]
		return true
	}
	for !done() {}
}

In the above example, there are ten "reader" goroutines created, each of them modifies one element of values. The ten "reader" goroutines don't interference with each other, The done goroutine should be viewed as a general accessor.

Please note that m.Lock(), m.Unlock(), m.RLock() and c.RUnlock() in the above two examples are shorthands of (&m).Lock(), (&m).Unlock(), (&m).RLock() and (&m).RUnlock(), respectively.

sync.Mutex values can also be used to make notifications, though there are many other better ways to do the same jobs. Here is an example which makes a notification by using a sync.Mutex value.
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var m sync.Mutex
	m.Lock()
	go func() {
		time.Sleep(time.Second)
		fmt.Println("Hi.")
		m.Unlock() // make a notification
	}()
	m.Lock() // wait to be notified
	fmt.Println("Bye.")
}

In the above example, the text Hi. is guaranteed to be printed before the text Bye.. About the memory order guarantees made by sync.Mutex and sync.RWMutex values, please read memory order guarantees in Go.

The sync.Cond Type

The sync.Cond type provides an efficient way to do notifications among goroutines.

Each sync.Cond value holds sync.Locker field with name L. The field value is often a value of *sync.Mutex or *sync.RWMutex. Each sync.Cond value also meantains a waiting goroutine queue.

The *sync.Cond type has three methods, Wait, Signal and Broadcast.

For an addressable sync.Cond value c, (1) This is for the most common case, by assuming that c.Signal() and c.Broadcast() are not called in the short period between the step 2 and step 3.

Please note that c.Wait(), c.Signal() and c.Broadcast() are shorthands of (&c).Wait(), (&c).Signal() and (&c).Broadcast(), respectively.

In an idiomatic sync.Cond use case, generally, one goroutine waits for changes of a certain condition, and some other goroutines make codition change notifications. Here is an example:
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	fooIsDone = false // Here, the user defined condition
	barIsDone = false // is composed of two parts.
	cond      = sync.NewCond(&sync.Mutex{})
)

func doFoo() {
	time.Sleep(time.Second) // simulate a workload
	cond.L.Lock()
	fooIsDone = true
	cond.Signal() // called when cond.L locker is acquired
	cond.L.Unlock()
}

func doBar() {
	time.Sleep(time.Second * 2) // simulate a workload
	cond.L.Lock()
	barIsDone = true
	cond.L.Unlock()
	cond.Signal() // called when cond.L locker is released
}

func main() {
	cond.L.Lock()
	
	go doFoo()
	go doBar()

	checkConditon := func() bool {
		fmt.Println(fooIsDone, barIsDone)
		return fooIsDone && barIsDone
	}
	for !checkConditon() {
		cond.Wait()
	}

	cond.L.Unlock()
}
The output:
false false
true false
true true

For there is only one goroutine (the main goroutine) waiting to be unblocked, the two cond.Signal() calls can be replaced with cond.Broadcast().

Some details in the above example. The user defined condition monitored by a sync.Cond value can be a void. For such cases, the sync.Cond value is used for notifications purely. For example, the following program will print abc or bac.
package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	wg.Add(1)
	cond := sync.NewCond(&sync.Mutex{})
	cond.L.Lock()
	go func() {
		cond.L.Lock()
		go func() {
			cond.L.Lock()
			cond.Broadcast()
			cond.L.Unlock()
		}()
		cond.Wait()
		fmt.Print("a")
		cond.L.Unlock()
		wg.Done()
	}()
	cond.Wait()
	fmt.Print("b")
	cond.L.Unlock()
	wg.Wait()
	fmt.Println("c")
}

If it needs, multiple sync.Cond values can share the same sync.Locker. However, such cases are rare in practice.


The Go 101 project is hosted on both Github and Gitlab. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

Support Go 101 by playing Tapir's games. Cryptocurrency donations are also welcome:
Bitcoin: 1xucQbv5jujFPPwhyg395ri5yV71hx9g9
Ethereum: 0x5dc4aa2c2bbfaadae373dadcfca11b3358912212