현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

Concurrency Synchronization Techniques Provided in the sync Standard Package

The channel use cases article introduces many use cases in which 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 specified 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 initial value of the counter is zero.

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

For an addressable 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 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 (
	"log"
	"math/rand"
	"sync"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano()) // needed before Go 1.20

	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)
			log.Println("Done:", i)
			wg.Done() // <=> wg.Add(-1)
		}()
	}

	wg.Wait()
	// All elements are guaranteed to be
	// initialized now.
	log.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 main() {
	rand.Seed(time.Now().UnixNano()) // needed before Go 1.20

	const N = 5
	var values [N]int32

	var wgA, wgB sync.WaitGroup
	wgA.Add(N)
	wgB.Add(1)

	for i := 0; i < N; i++ {
		i := i
		go func() {
			wgB.Wait() // wait a notification
			log.Printf("values[%v]=%v \n", i, values[i])
			wgA.Done()
		}()
	}

	// 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)
	}
	// Make a broadcast notification.
	wgB.Done()
	wgA.Wait()
}

A WaitGroup value can be reused after one call to its Wait method returns. But please note that each Add method call with a positive delta that occurs when the counter is zero must happen before any Wait call starts, otherwise, data races may happen.

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 Once value o, the method call o.Do(), which is a shorthand of (&o).Do(), can be concurrently executed multiple times, in multiple goroutines. The arguments of these o.Do() calls should (but are not required to) be the same function value.

Among these o.Do method calls, only exact one argument function will be invoked. The invoked argument function is guaranteed to exit 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, a Once value is used to ensure that a piece of code will be executed exactly 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. So they both have two methods, Lock and Unlock, to prevent multiple data users from using a piece of data concurrently.

Besides the Lock and Unlock methods, the *RWMutex type has two other methods, RLock and RUnlock, to avoid some data users (either writers or readers) and one data writer using a piece of data at the same time but allow some data readers to access the piece of data at the same time.

(Note, here the terminologies data reader and data writer should not be interpreted from literal. They are just used for explanation purpose. A data reader might modify data and a data writer might only read data.)

A Mutex value is often called a mutual exclusion lock. A zero Mutex value is an unlocked mutex. A Mutex value can only be locked when it is in unlocked status. In other words, once an addressable Mutex value m is locked successfully (a.k.a., a m.Lock() method call returns), a new attempt by a goroutine to lock the Mutex value will make the goroutine enter blocking state, until the Mutex value is unlocked (through a later m.Unlock() call).

Please note that m.Lock() and m.Unlock() are shorthands of (&m).Lock() and (&m).Unlock(), respectively.

An example of using sync.Mutex:
package main

import (
	"fmt"
	"runtime"
	"sync"
)

type Counter struct {
	m sync.Mutex
	n uint64
}

func (c *Counter) Value() uint64 {
	c.m.Lock()
	defer c.m.Unlock()
	return c.n
}

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

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

	// The loop is just for demo purpose.
	for c.Value() < 10000 {
		runtime.Gosched()
	}
	fmt.Println(c.Value()) // 10000
}

In the above example, a Counter value uses a Mutex field to guarantee that the n field of the Counter value will be never used by multiple goroutines at the same time.

A RWMutex value is often called a reader+writer mutual exclusion lock. It has two locks, the write lock and the read lock. The locks of a zero RWMutex value are both unlocked. For an addressable RWMutex value rwm, data writers can lock the write lock of rwm through rwm.Lock() method calls, and data readers can lock the read lock of rwm through rwm.RLock() method calls. Method calls rwm.Unlock() and rwm.RUnlock() are used to unlock the write and read locks of rwm. The read lock of rwm maintains a lock count, which increases by one when rwm.Lock() is called successfully and decreases by one when rwm.Unlock() is called successfully. A zero lock count means the read lock is in unlocked status and a non-zero one (must be larger than one) means the read lock is locked.

Please note that rwm.Lock(), rwm.Unlock(), rwm.RLock() and rwm.RUnlock() are shorthands of (&m).Lock(), (&m).Unlock(), (&m).RLock() and (&m).RUnlock(), respectively.

For an addressable RWMutex value rwm, the following rules exist.

The last two rules are to ensure both readers and writers have chances to acquire locks.

Please note, locks are not bound to goroutines. A lock may be locked in one goroutine and unlocked in another one later. In other words, a lock doesn't know which goroutine successfully locked or unlocked it.

The type of the m field of the Counter type in the last example can be changed to sync.RWMutex, as the following code shows, to get a better performance when the Value method is called very frequently but the Increase method is called not frequently.
...
type Counter struct {
	//m sync.Mutex
	m sync.RWMutex
	n uint64
}

func (c *Counter) Value() uint64 {
	//c.m.Lock()
	//defer c.m.Unlock()
	c.m.RLock()
	defer c.m.RUnlock()
	return c.n
}
...

Another use scenario of sync.RWMutex values is to slice a write job into several small ones. Please read the next section for an example.

By the last two rules mentioned above, the following program is very possible to output abdc.
package main

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

func main() {
	var m sync.RWMutex
	go func() {
		m.RLock()
		fmt.Print("a")
		time.Sleep(time.Second)
		m.RUnlock()
	}()
	go func() {
		time.Sleep(time.Second * 1 / 4)
		m.Lock()
		fmt.Print("b")
		time.Sleep(time.Second)
		m.Unlock()
	}()
	go func() {
		time.Sleep(time.Second * 2 / 4)
		m.Lock()
		fmt.Print("c")
		m.Unlock()
	}()
	go func () {
		time.Sleep(time.Second * 3 / 4)
		m.RLock()
		fmt.Print("d")
		m.RUnlock()
	}()
	time.Sleep(time.Second * 3)
	fmt.Println()
}

Please note, the above example is only for explanation purpose. It uses time.Sleep calls to do concurrency synchronizations, which is a bad practice for production code.

sync.Mutex and sync.RWMutex values can also be used to make notifications, though there are many other better ways to do the same job. 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 a sync.Locker field with name L. The field value is often a value of type *sync.Mutex or *sync.RWMutex.

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

Each sync.Cond value also maintains a FIFO (first in first out) waiting goroutine queue. For an addressable sync.Cond value c,

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

c.Signal() and c.Broadcast() are often used to notify the status of a condition is changed, Generally, c.Wait() should be called in a loop of checking whether or not a condition has got satisfied.

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

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

func main() {
	rand.Seed(time.Now().UnixNano()) // needed before Go 1.20

	const N = 10
	var values [N]string

	cond := sync.NewCond(&sync.Mutex{})

	for i := 0; i < N; i++ {
		d := time.Second * time.Duration(rand.Intn(10)) / 10
		go func(i int) {
			time.Sleep(d) // simulate a workload

			// Changes must be made when
			// cond.L is locked.
			cond.L.Lock()
			values[i] = string('a' + i)

			// Notify when cond.L lock is locked.
			cond.Broadcast()
			cond.L.Unlock()

			// "cond.Broadcast()" can also be put
			// here, when cond.L lock is unlocked.
			//cond.Broadcast()
		}(i)
	}

	// This function must be called when
	// cond.L is locked.
	checkCondition := func() bool {
		fmt.Println(values)
		for i := 0; i < N; i++ {
			if values[i] == "" {
				return false
			}
		}
		return true
	}

	cond.L.Lock()
	defer cond.L.Unlock()
	for !checkCondition() {
		// Must be called when cond.L is locked.
		cond.Wait()
	}
}
One possible output:
[         ]
[     f    ]
[  c   f    ]
[  c   f  h  ]
[ b c   f  h  ]
[a b c   f  h  j]
[a b c   f g h i j]
[a b c  e f g h i j]
[a b c d e f g h i j]

For there is only one goroutine (the main goroutine) waiting to be unblocked in this example, the cond.Broadcast() call can be replaced with cond.Signal(). As the comments suggest, cond.Broadcast() and cond.Signal() are not required to be called when cond.L is locked.

To avoid data races, each of the ten parts of the user defined condition should only be modified when cond.L is locked. The checkCondition function and the cond.Wait method should be also called when cond.L is locked.

In fact, for the above specified example, the cond.L field can also be a *sync.RWMutex value, and each of the ten parts of the user defined condition can be modified when the read lock of cond.L is held, just as the following code shows:

...
	cond := sync.NewCond(&sync.RWMutex{})
	cond.L.Lock()

	for i := 0; i < N; i++ {
		d := time.Second * time.Duration(rand.Intn(10)) / 10
		go func(i int) {
			time.Sleep(d)
			cond.L.(*sync.RWMutex).RLock()
			values[i] = string('a' + i)
			cond.L.(*sync.RWMutex).RUnlock()
			cond.Signal()
		}(i)
	}
...

In the above example, the sync.RWMutex value is used unusually. Its read lock is held by some goroutines which modify array elements, and its write lock is used by the main goroutine to read array elements.

The user defined condition monitored by a Cond value can be a void. For such cases, the 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.


Index↡

The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: