Go语言竞争状态简述
Go语言中如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。
竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
【示例】包含竞争状态的示例程序。
// 这个示例程序展示如何在程序里造成竞争状态
// 实际上不希望出现这种情况
package main
import (
"fmt"
"runtime"
"sync"
)
var (
// counter 是所有goroutine 都要增加其值的变量
counter int
// wg 用来等待程序结束
wg sync.WaitGroup
)
// main 是所有Go 程序的入口
func main() {
// 计数加 2,表示要等待两个goroutine
wg.Add(2)
// 创建两个goroutine
go incCounter(1)
go incCounter(2)
// 等待 goroutine 结束
wg.Wait()
fmt.Println("Final Counter:", counter)
}
// incCounter 增加包里counter 变量的值
func incCounter(id int) {
// 在函数退出时调用Done 来通知main 函数工作已经完成
defer wg.Done()
for count := 0; count < 2; count++ {
// 捕获 counter 的值
value := counter
// 当前 goroutine 从线程退出,并放回到队列
runtime.Gosched()
// 增加本地value 变量的值
value++
// 将该值保存回counter
counter = value
}
}
输出结果如下所示。
Final Counter: 2
变量 counter 会进行 4 次读和写操作,每个 goroutine 执行两次。但是,程序终止时,counter 变量的值为 2。下图提供了为什么会这样的线索。
图:竞争状态下程序行为的图像表达
每个 goroutine 都会覆盖另一个 goroutine 的工作。这种覆盖发生在goroutine 切换的时候。每个 goroutine 创造了一个 counter 变量的副本,之后就切换到另一个 goroutine。当这个 goroutine 再次运行的时候,counter 变量的值已经改变了,但是 goroutine 并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回 counter 变量,结果覆盖了另一个 goroutine 完成的工作。
代码说明如下:
在第 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语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的示例代码,代码如下所示。
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)
上述代码中的竞争检测器指出了 4 行代码有问题,如下所示。
Line 49: counter = value
Line 40: value := counter
Line 25: go incCounter(1)
Line 26: go incCounter(2)
上面展示了竞争检测器查到的哪个 goroutine 引发了数据竞争,以及哪两行代码有冲突。毫不奇怪,这几行代码分别是对 counter 变量的读和写操作。一种修正代码、消除竞争状态的办法是,使用 Go语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。
还没有评论,来说两句吧...