6.4.2 互斥锁
另一种同步访问共享资源的方式是使用互斥锁( mutex
)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。我们还可以用互斥锁来修正代码清单6-9中创建的竞争状态,如代码清单6-16所示。
代码清单6-16 listing16.go
01 // 这个示例程序展示如何使用互斥锁来
02 // 定义一段需要同步访问的代码临界区
03 // 资源的同步访问
04 package main
05
06 import (
07 "fmt"
08 "runtime"
09 "sync"
10 )
11
12 var (
13 // counter是所有goroutine都要增加其值的变量
14 counter int
15
16 // wg用来等待程序结束
17 wg sync.WaitGroup
18
19 // mutex 用来定义一段代码临界区
20 mutex sync.Mutex
21 )
22
23 // main是所有Go程序的入口
24 func main() {
25 // 计数加2,表示要等待两个goroutine
26 wg.Add(2)
27
28 // 创建两个goroutine
29 go incCounter(1)
30 go incCounter(2)
31
32 // 等待goroutine结束
33 wg.Wait()
34 fmt.Printf("Final Counter: %d\\n", counter)
35 }
36
37 // incCounter使用互斥锁来同步并保证安全访问,
38 // 增加包里counter变量的值
39 func incCounter(id int) {
40 // 在函数退出时调用Done来通知main函数工作已经完成
41 defer wg.Done()
42
43 for count := 0; count < 2; count++ {
44 // 同一时刻只允许一个goroutine进入
45 // 这个临界区
46 mutex.Lock()
47 {
48 // 捕获counter的值
49 value := counter
50
51 // 当前goroutine从线程退出,并放回到队列
52 runtime.Gosched()
53
54 // 增加本地value变量的值
55 value++
56
57 // 将该值保存回counter
58 counter = value
59 }
60 mutex.Unlock()
61 // 释放锁,允许其他正在等待的goroutine
62 // 进入临界区
63 }
64 }
对 counter
变量的操作在第46行和第60行的 Lock()
和 Unlock()
函数调用定义的临界区里被保护起来。使用大括号只是为了让临界区看起来更清晰,并不是必需的。同一时刻只有一个goroutine可以进入临界区。之后,直到调用 Unlock()
函数之后,其他goroutine才能进入临界区。当第52行强制将当前goroutine退出当前线程后,调度器会再次分配这个goroutine继续运行。当程序结束时,我们得到正确的值 4
,竞争状态不再存在。