6.3 竞争状态
如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作 竞争状态 (race candition)。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。代码清单 6-9 中给出的是包含竞争状态的示例程序。
代码清单 6-9 listing09.go
01 // 这个示例程序展示如何在程序里造成竞争状态
02 // 实际上不希望出现这种情况
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 )
10
11 var (
12 // counter 是所有 goroutine 都要增加其值的变量
13 counter int
14
15 // wg 用来等待程序结束
16 wg sync.WaitGroup
17 )
18
19 // main 是所有 Go 程序的入口
20 func main() {
21 // 计数加 2,表示要等待两个 goroutine
22 wg.Add(2)
23
24 // 创建两个 goroutine
25 go incCounter(1)
26 go incCounter(2)
27
28 // 等待 goroutine 结束
29 wg.Wait()
30 fmt.Println("Final Counter:", counter)
31 }
32
33 // incCounter 增加包里 counter 变量的值
34 func incCounter(id int) {
35 // 在函数退出时调用 Done 来通知 main 函数工作已经完成
36 defer wg.Done()
37
38 for count := 0; count < 2; count++ {
39 // 捕获 counter 的值
40 value := counter
41
42 // 当前 goroutine 从线程退出,并放回到队列
43 runtime.Gosched()
44
45 // 增加本地 value 变量的值
46 value++
47
48 // 将该值保存回 counter
49 counter = value
50 }
51 }
对应的输出如代码清单 6-10 所示。
代码清单 6-10 listing09.go 的输出
Final Counter: 2
变量 counter
会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时, counter
变量的值为 2。图 6-5 提供了为什么会这样的线索。
每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在 goroutine 切换的时候。每个 goroutine 创造了一个 counter
变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候, counter
变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter
变量,结果覆盖了另一个 goroutine 完成的工作。
图 6-5 竞争状态下程序行为的图像表达
让我们顺着程序理解一下发生了什么。在第 25 行和第 26 行,使用 incCounter
函数创建了两个 goroutine。在第 34 行, incCounter
函数对包内变量 counter
进行了读和写操作,而这个变量是这个示例程序里的共享资源。每个 goroutine 都会先读出这个 counter
变量的值,并在第 40 行将 counter
变量的副本存入一个叫作 value
的本地变量。之后在第 46 行, incCounter
函数对 value
的副本的值加 1,最终在第 49 行将这个新值存回到 counter
变量。这个函数在第 43 行调用了 runtime
包的 Gosched
函数,用于将 goroutine 从当前线程退出,给其他 goroutine 运行的机会。在两次操作中间这样做的目的是强制调度器切换两个 goroutine,以便让竞争状态的效果变得更明显。
Go 语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的例子代码,如代码清单 6-11 所示。
代码清单 6-11 用竞争检测器来编译并执行 listing09 的代码
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
==================
WARNING: DATA RACE
Write by goroutine 5:
main.incCounter()
/example/main.go:49 +0x96
Previous read by goroutine 6:
main.incCounter()
/example/main.go:40 +0x66
Goroutine 5 (running) created at:
main.main()
/example/main.go:25 +0x5c
Goroutine 6 (running) created at:
main.main()
/example/main.go:26 +0x73
==================
Final Counter: 2
Found 1 data race(s)
代码清单 6-11 中的竞争检测器指出这个例子里面代码清单 6-12 所示的 4 行代码有问题。
代码清单 6-12 竞争检测器指出的代码
Line 49: counter = value
Line 40: value := counter
Line 25: go incCounter(1)
Line 26: go incCounter(2)
代码清单 6-12 展示了竞争检测器查到的哪个 goroutine 引发了数据竞争,以及哪两行代码有冲突。毫不奇怪,这几行代码分别是对 counter
变量的读和写操作。
一种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论