6.4 锁住共享资源
Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码, atomic
和 sync
包里的函数提供了很好的解决方案。下面我们了解一下 atomic
包里的几个函数以及 sync
包里的 mutex
类型。
6.4.1 原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。我们可以用原子函数来修正代码清单 6-9 中创建的竞争状态,如代码清单 6-13 所示。
代码清单 6-13 listing13.go
01 // 这个示例程序展示如何使用 atomic 包来提供
02 // 对数值类型的安全访问
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 "sync/atomic"
10 )
11
12 var (
13 // counter 是所有 goroutine 都要增加其值的变量
14 counter int64
15
16 // wg 用来等待程序结束
17 wg sync.WaitGroup
18 )
19
20 // main 是所有 Go 程序的入口
21 func main() {
22 // 计数加 2,表示要等待两个 goroutine
23 wg.Add(2)
24
25 // 创建两个 goroutine
26 go incCounter(1)
27 go incCounter(2)
28
29 // 等待 goroutine 结束
30 wg.Wait()
31
32 // 显示最终的值
33 fmt.Println("Final Counter:", counter)
34 }
35
36 // incCounter 增加包里 counter 变量的值
37 func incCounter(id int) {
38 // 在函数退出时调用 Done 来通知 main 函数工作已经完成
39 defer wg.Done()
40
41 for count := 0; count < 2; count++ {
42 // 安全地对 counter 加 1
43 atomic.AddInt64(&counter, 1)
44
45 // 当前 goroutine 从线程退出,并放回到队列
46 runtime.Gosched()
47 }
48 }
对应的输出如代码清单 6-14 所示。
代码清单 6-14 listing13.go 的输出
Final Counter: 4
现在,程序的第 43 行使用了 atmoic
包的 AddInt64
函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。现在我们得到了正确的值 4。
另外两个有用的原子函数是 LoadInt64
和 StoreInt64
。这两个函数提供了一种安全地读和写一个整型值的方式。代码清单 6-15 中的示例程序使用 LoadInt64
和 StoreInt64
来创建一个同步标志,这个标志可以向程序里多个 goroutine 通知某个特殊状态。
代码清单 6-15 listing15.go
01 // 这个示例程序展示如何使用 atomic 包里的
02 // Store 和 Load 类函数来提供对数值类型
03 // 的安全访问
04 package main
05
06 import (
07 "fmt"
08 "sync"
09 "sync/atomic"
10 "time"
11 )
12
13 var (
14 // shutdown 是通知正在执行的 goroutine 停止工作的标志
15 shutdown int64
16
17 // wg 用来等待程序结束
18 wg sync.WaitGroup
19 )
20
21 // main 是所有 Go 程序的入口
22 func main() {
23 // 计数加 2,表示要等待两个 goroutine
24 wg.Add(2)
25
26 // 创建两个 goroutine
27 go doWork("A")
28 go doWork("B")
29
30 // 给定 goroutine 执行的时间
31 time.Sleep(1 * time.Second)
32
33 // 该停止工作了,安全地设置 shutdown 标志
34 fmt.Println("Shutdown Now")
35 atomic.StoreInt64(&shutdown, 1)
36
37 // 等待 goroutine 结束
38 wg.Wait()
39 }
40
41 // doWork 用来模拟执行工作的 goroutine,
42 // 检测之前的 shutdown 标志来决定是否提前终止
43 func doWork(name string) {
44 // 在函数退出时调用 Done 来通知 main 函数工作已经完成
45 defer wg.Done()
46
47 for {
48 fmt.Printf("Doing %s Work\n", name)
49 time.Sleep(250 * time.Millisecond)
50
51 // 要停止工作了吗?
52 if atomic.LoadInt64(&shutdown) == 1 {
53 fmt.Printf("Shutting %s Down\n", name)
54 break
55 }
56 }
57 }
在这个例子中,启动了两个 goroutine,并完成一些工作。在各自循环的每次迭代之后,在第 52 行中 goroutine 会使用 LoadInt64
来检查 shutdown
变量的值。这个函数会安全地返回 shutdown
变量的一个副本。如果这个副本的值为 1,goroutine 就会跳出循环并终止。
在第 35 行中, main
函数使用 StoreInt64
函数来安全地修改 shutdown
变量的值。如果哪个 doWork
goroutine 试图在 main
函数调用 StoreInt64
的同时调用 LoadInt64
函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。
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
,竞争状态不再存在。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论