# `sync`标准库包中提供的并发同步技术

通道用例大全一文中介绍了很多通过使用通道来实现并发同步的用例。 事实上，通道并不是Go支持的唯一的一种并发同步技术。而且对于一些特定的情形，通道并不是最有效和可读性最高的同步技术。 本文下面将介绍`sync`标准库包中提供的各种并发同步技术。相对于通道，这些技术对于某些情形更加适用。

`sync`标准库包提供了一些用于实现并发同步的类型。这些类型适用于各种不同的内存顺序需求。 对于这些特定的需求，这些类型使用起来比通道效率更高，代码实现更简洁。

（请注意：为了避免各种异常行为，最好不要复制`sync`标准库包中提供的类型的值。）

### `sync.WaitGroup`（等待组）类型

每个`sync.WaitGroup`值在内部维护着一个计数，此计数的初始默认值为零。

`*sync.WaitGroup`类型有三个方法：`Add(delta int)`、`Done()`和`Wait()`。

对于一个可寻址的`sync.WaitGroup`值`wg`，

- 我们可以使用方法调用`wg.Add(delta)`来改变值`wg`维护的计数。
- 方法调用`wg.Done()`和`wg.Add(-1)`是完全等价的。
- 如果一个`wg.Add(delta)`或者`wg.Done()`调用将`wg`维护的计数更改成一个负数，一个恐慌将产生。
- 当一个协程调用了`wg.Wait()`时，
  - 如果此时`wg`维护的计数为零，则此`wg.Wait()`此操作为一个空操作（no-op）；
  - 否则（计数为一个正整数），此协程将进入阻塞状态。 当以后其它某个协程将此计数更改至0时（一般通过调用`wg.Done()`），此协程将重新进入运行状态（即`wg.Wait()`将返回）。

请注意`wg.Add(delta)`、`wg.Done()`和`wg.Wait()`分别是`(&wg).Add(delta)`、`(&wg).Done()`和`(&wg).Wait()`的简写形式。

一般，一个`sync.WaitGroup`值用来让某个协程等待其它若干协程都先完成它们各自的任务。 一个例子：

    package main

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

    func main() {
    	rand.Seed(time.Now().UnixNano()) // 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)
    			fmt.Println("Done:", i)
    			wg.Done() // <=> wg.Add(-1)
    		}()
    	}

    	wg.Wait()
    	// 所有的元素都保证被初始化了。
    	fmt.Println("values:", values)
    }

在此例中，主协程等待着直到其它5个协程已经将各自负责的元素初始化完毕此会打印出各个元素值。 这里是一个可能的程序执行输出结果：

    Done: 4
    Done: 1
    Done: 3
    Done: 0
    Done: 2
    values: [71 89 50 62 60]

我们可以将上例中的`Add`方法调用拆分成多次调用：

    ...
    	var wg sync.WaitGroup
    	for i := 0; i < N; i++ {
    		wg.Add(1) // 将被执行5次
    		i := i
    		go func() {
    			values[i] = 50 + rand.Int31n(50)
    			wg.Done()
    		}()
    	}
    ...

一个`*sync.WaitGroup`值的`Wait`方法可以在多个协程中调用。 当对应的`sync.WaitGroup`值维护的计数降为0，这些协程都将得到一个（广播）通知而结束阻塞状态。

    func main() {
    	rand.Seed(time.Now().UnixNano()) // 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() // 等待广播通知
    			log.Printf("values[%v]=%v \n", i, values[i])
    			wgA.Done()
    		}()
    	}

    	// 下面这个循环保证将在上面的任何一个
    	// wg.Wait调用结束之前执行。
    	for i := 0; i < N; i++ {
    		values[i] = 50 + rand.Int31n(50)
    	}
    	wgB.Done() // 发出一个广播通知
    	wgA.Wait()
    }

一个`WaitGroup`可以在它的一个`Wait`方法返回之后被重用。 但是请注意，当一个`WaitGroup`值维护的基数为零时，它的带有正整数实参的`Add`方法调用不能和它的`Wait`方法调用并发运行，否则将可能出现数据竞争。

### `sync.Once`类型

每个`*sync.Once`值有一个`Do(f func())`方法。 此方法只有一个类型为`func()`的参数。

对一个可寻址的`sync.Once`值`o`，`o.Do()`（即`(&o).Do()`的简写形式）方法调用可以在多个协程中被多次并发地执行， 这些方法调用的实参应该（但并不强制）为同一个函数值。 在这些方法调用中，有且只有一个调用的实参函数（值）将得到调用。 此被调用的实参函数保证在任何`o.Do()`方法调用返回之前退出。 换句话说，被调用的实参函数内的代码将在任何`o.Do()`方法返回调用之前被执行。

一般来说，一个`sync.Once`值被用来确保一段代码在一个并发程序中被执行且仅被执行一次。

一个例子：

    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
    }

在此例中，`Hello`将仅被输出一次，而`world!`将被输出5次，并且`Hello`肯定在所有的5个`world!`之前输出。

### `sync.Mutex`（互斥锁）和`sync.RWMutex`（读写锁）类型

`*sync.Mutex`和`*sync.RWMutex`类型都实现了`sync.Locker`接口类型。 所以这两个类型都有两个方法：`Lock()`和`Unlock()`，用来保护一份数据不会被多个使用者同时读取和修改。

除了`Lock()`和`Unlock()`这两个方法，`*sync.RWMutex`类型还有两个另外的方法：`RLock()`和`RUnlock()`，用来支持多个读取者并发读取一份数据但防止此份数据被某个数据写入者和其它数据访问者（包括读取者和写入者）同时使用。

（注意：这里的数据读取者和数据写入者不应该从字面上理解。有时候某些数据读取者可能修改数据，而有些数据写入者可能只读取数据。）

一个`Mutex`值常称为一个互斥锁。 一个`Mutex`零值为一个尚未加锁的互斥锁。 一个（可寻址的）`Mutex`值`m`只有在未加锁状态时才能通过`m.Lock()`方法调用被成功加锁。 换句话说，一旦`m`值被加了锁（亦即某个`m.Lock()`方法调用成功返回）， 一个新的加锁试图将导致当前协程进入阻塞状态，直到此`Mutex`值被解锁为止（通过`m.Unlock()`方法调用）。

注意：`m.Lock()`和`m.Unlock()`分别是`(&m).Lock()`和`(&m).Unlock()`的简写形式。

一个使用`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)
    			}
    		}()
    	}

    	// 此循环仅为演示目的。
    	for c.Value() < 10000 {
    		runtime.Gosched()
    	}
    	fmt.Println(c.Value()) // 10000
    }

在上面这个例子中，一个`Counter`值使用了一个`Mutex`字段来确保它的字段`n`永远不会被多个协程同时使用。

一个`RWMutex`值常称为一个读写互斥锁，它的内部包含两个锁：一个写锁和一个读锁。 对于一个可寻址的`RWMutex`值`rwm`，数据写入者可以通过方法调用`rwm.Lock()`对`rwm`加写锁，或者通过`rwm.RLock()`方法调用对`rwm`加读锁。 方法调用`rwm.Unlock()`和`rwm.RUnlock()`用来解开`rwm`的写锁和读锁。 `rwm`的读锁维护着一个计数。当`rwm.RLock()`调用成功时，此计数增1；当`rwm.Unlock()`调用成功时，此计数减1； 一个零计数表示`rwm`的读锁处于未加锁状态；反之，一个非零计数（肯定大于零）表示`rwm`的读锁处于加锁状态。

注意`rwm.Lock()`、`rwm.Unlock()`、`rwm.RLock()`和`rwm.RUnlock()`分别是`(&rwm).Lock()`、`(&rwm).Unlock()`、`(&rwm).RLock()`和`(&rwm).RUnlock()`的简写形式。

对于一个可寻址的`RWMutex`值`rwm`，下列规则存在：

- `rwm`的写锁只有在它的写锁和读锁都处于未加锁状态时才能被成功加锁。 换句话说，`rwm`的写锁在任何时刻最多只能被一个数据写入者成功加锁，并且`rwm`的写锁和读锁不能同时处于加锁状态。
- 当`rwm`的写锁正处于加锁状态的时候，任何新的对之加写锁或者加读锁的操作试图都将导致当前协程进入阻塞状态，直到此写锁被解锁，这样的操作试图才有机会成功。
- 当`rwm`的读锁正处于加锁状态的时候，新的加写锁的操作试图将导致当前协程进入阻塞状态。 但是，一个新的加读锁的操作试图将成功，只要此操作试图发生在任何被阻塞的加写锁的操作试图之前（见下一条规则）。 换句话说，一个读写互斥锁的读锁可以同时被多个数据读取者同时加锁而持有。 当`rwm`的读锁维护的计数清零时，读锁将返回未加锁状态。
- 假设`rwm`的读锁正处于加锁状态的时候，为了防止后续数据写入者没有机会成功加写锁，后续发生在某个被阻塞的加写锁操作试图之后的所有加读锁的试图都将被阻塞。
- 假设`rwm`的写锁正处于加锁状态的时候，（至少对于标准编译器来说，）为了防止后续数据读取者没有机会成功加读锁，发生在此写锁下一次被解锁之前的所有加读锁的试图都将在此写锁下一次被解锁之后肯定取得成功，即使所有这些加读锁的试图发生在一些仍被阻塞的加写锁的试图之后。

后两条规则是为了确保数据读取者和写入者都有机会执行它们的操作。

请注意：一个锁并不会绑定到一个协程上，即一个锁并不记录哪个协程成功地加锁了它。 换句话说，一个锁的加锁者和此锁的解锁者可以不是同一个协程，尽管在实践中这种情况并不多见。

在上一个例子中，如果`Value`方法被十分频繁调用而`Increase`方法并不频繁被调用，则`Counter`类型的`m`字段的类型可以更改为`sync.RWMutex`，从而使得执行效率更高，如下面的代码所示。

    ...
    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
    }
    ...

`sync.RWMutex`值的另一个应用场景是将一个写任务分隔成若干小的写任务。下一节中展示了一个这样的例子。

根据上面列出的后两条规则，下面这个程序最有可能输出`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()
    }

请注意，上例这个程序仅仅是为了解释和验证上面列出的读写锁的后两条加锁规则。 此程序使用了`time.Sleep`调用来做协程间的同步。这种所谓的同步方法不应该被使用在生产代码中。

`sync.Mutex`和`sync.RWMutex`值也可以用来实现通知，尽管这不是Go中最优雅的方法来实现通知。 下面是一个使用了`Mutex`值来实现通知的例子。

    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() // 发出一个通知
    	}()
    	m.Lock() // 等待通知
    	fmt.Println("Bye")
    }

在此例中，`Hi`将确保在`Bye`之前打印出来。 关于`sync.Mutex`和`sync.RWMutex`值相关的内存顺序保证，请阅读Go中的内存顺序保证一文。

### `sync.Cond`类型

`sync.Cond`类型提供了一种有效的方式来实现多个协程间的通知。

每个`sync.Cond`值拥有一个`sync.Locker`类型的名为`L`的字段。 此字段的具体值常常为一个`*sync.Mutex`值或者`*sync.RWMutex`值。

`*sync.Cond`类型有三个方法：`Wait()`、`Signal()`和`Broadcast()`。

每个`Cond`值维护着一个先进先出等待协程队列。 对于一个可寻址的`Cond`值`c`，

- `c.Wait()`必须在`c.L`字段值的锁处于加锁状态的时候调用；否则，`c.Wait()`调用将造成一个恐慌。 一个`c.Wait()`调用将
  1. 首先将当前协程推入到`c`所维护的等待协程队列；
  2. 然后调用`c.L.Unlock()`对`c.L`的锁解锁；

  3. 然后使当前协程进入阻塞状态；

     （当前协程将被另一个协程通过`c.Signal()`或`c.Broadcast()`调用唤醒而重新进入运行状态。）

     一旦当前协程重新进入运行状态，`c.L.Lock()`将被调用以试图重新对`c.L`字段值的锁加锁。 此`c.Wait()`调用将在此试图成功之后退出。
- 一个`c.Signal()`调用将唤醒并移除`c`所维护的等待协程队列中的第一个协程（如果此队列不为空的话）。
- 一个`c.Broadcast()`调用将唤醒并移除`c`所维护的等待协程队列中的所有协程（如果此队列不为空的话）。

请注意：`c.Wait()`、`c.Signal()`和`c.Broadcast()`分别为`(&c).Wait()`、`(&c).Signal()`和`(&c).Broadcast()`的简写形式。

`c.Signal()`和`c.Broadcast()`调用常用来通知某个条件的状态发生了变化。 一般说来，`c.Wait()`应该在一个检查某个条件是否已经得到满足的循环中调用。

下面是一个典型的`sync.Cond`用例。

    package main

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

    func main() {
    	rand.Seed(time.Now().UnixNano()) // 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) // 模拟一个工作负载
    			cond.L.Lock()
    			// 下面的修改必须在cond.L被锁定的时候执行
    			values[i] = string('a' + i)
    			cond.Broadcast() // 可以在cond.L被解锁后发出通知
    			cond.L.Unlock()
    			// 上面的通知也可以在cond.L未锁定的时候发出。
    			//cond.Broadcast() // 上面的调用也可以放在这里
    		}(i)
    	}

    	// 此函数必须在cond.L被锁定的时候调用。
    	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() {
    		cond.Wait() // 必须在cond.L被锁定的时候调用
    	}
    }

一个可能的输出：

    [         ]
    [     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]

因为上例中只有一个协程（主协程）在等待通知，所以其中的`cond.Broadcast()`调用也可以换为`cond.Signal()`。 如上例中的注释所示，`cond.Broadcast()`和`cond.Signal()`不必在`cond.L`的锁处于加锁状态时调用。

为了防止数据竞争，对自定义条件的修改必须在`cond.L`的锁处于加锁状态时才能执行。 另外，`checkCondition`函数和`cond.Wait`方法也必须在`cond.L`的锁处于加锁状态时才可被调用。

事实上，对于上面这个特定的例子，`cond.L`字段的也可以为一个`*sync.RWMutex`值。 对自定义条件的十个部分的修改可以在`RWMutex`值的读锁处于加锁状态时执行。这十个修改可以并发进行，因为它们是互不干扰的。 如下面的代码所示：

    ...
    	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)
    	}
    ...

在上面的代码中，此`sync.RWMutex`值的用法有些不符常规。 它的读锁被一些修改数组元素的协程所加锁并持有，而它的写锁被主协程加锁持有用来读取并检查各个数组元素的值。

`Cond`值所表示的自定义条件可以是一个虚无。对于这种情况，此`Cond`值纯粹被用来实现通知。 比如，下面这个程序将打印出`abc`或者`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")
    }

如果需要，多个`sync.Cond`值可以共享一个`sync.Locker`值。但是这种情形在实践中并不多见。