Goroutine 是Go 最重要的特性之一,它可以让开发者轻易做到并发(concurrency),而且他的非常轻量,所以一次开一大堆goroutine 也不会有什么问题。
但如果在使用goroutine 时没有考虑到race condition,那可能就会导致不正确的结果,这篇文章会用一个简单的小例子,谈谈在什么情况可能会遇到race condition,以及如何发现、解决它。
Race condition (竞争危害)
来看看这次要讲解的例子,分成三个步骤
- 先把a 的初始值设为 0
- 开三个goroutine 共做了三次 a++
- 最后用channel 等待三个goroutine 完成
没意外的话最后应该会得到a = 3,结果也确实如此
那如果把次数改成 一万次 呢?
理论上要得到a = 10000,实际跑了却会得到a = 9903之类的结果(如果你有多核CPU的话),但我们确实开了一万个goroutine也做了一万次a++,为什么结果会不对呢,因为在a++的过程中发生了race condition
为什么a++ 会发生race condition
当你写了a++时电脑实际上做了三件事:
- CPU 把a 的值取出来
- 把刚刚取得的值加 1
- 把运算的结果存回变数 a
但万一你有 多核CPU 就有可能会这样:
两个CPU 同时去拿变数a 的值,各自加1 后存回,导致a 只被加了一次,因此结果(9903)会小于正确的10000
解法:互斥锁
这里会发生race condition最根本的原因是「两个goroutine可能会同时存取变数a」,如果能限制 同时只能有一个 goroutine做a++,那就能解决这个问题,为了达到这个目的我们要使用sync.Mutex
Mutex是互斥锁的意思,同时最多只能有一个人拿到这个锁,其他人想要锁的话就会被block住直到前一个人用完,所以就可以确保只有一个goroutine正在进行a++,这样就可以得到正确的结果10000
如何发现race condition
在这个例子中race condition发生在a++,但如果对电脑底层不够熟悉就有可能没办法发现问题,还好Golang有个很强大的工具叫Data Race Detector
在跑的时候加一个-race他就可以帮你侦测哪边可能会产生race condiction,大家也可以自己下载 原码 下来跑
$ go run -race add_few_times/main.go ================== WARNING: DATA RACE Read at 0x00c4200a4008 by goroutine 7: main.main.func1() .../ add_few_times/main.go:12 +0x38 Previous write at 0x00c4200a4008 by goroutine 6: main.main.func1() .../add_few_times/main.go:12 +0x4e Goroutine 7 (running) created at: main.main() .../add_few_times/main.go:11 +0xc1 Goroutine 6 (running) created at: main.main() .../add_few_times/main.go:11 +0xc1 ================== Found 1 data race(s ) exit status 66
Race Detector说在第11行(go func(){…}())产生了两个goroutine( G6 跟 G7 ),G7在第十二行(a++)读取了变数a之后,G6紧接着写入了变数a,所以G7会读到旧资料,这时候就有可能会产生race condition
透过Race Detector 几乎可以找到所有的race condition,大部分时候也都只要加个锁就可以解决
锁的缺点
性能
上面的例子用mutex 来防止多个goroutine 同时存取同一个变数,因为总共有一万个goroutine,当你有其中一个正在存取a 时其他9999 个都在等他,他们之间完全没有并行( parallelism),不如用个回圈把它从0 加到10000 可能还更快
所以在使用锁时一定要非常小心,只在必要的时候使用,否则效能将会大打折扣
忘记解锁
有时候上锁解锁不像上面a++这么简单,中间可能有很多个锁还有各种条件判断、网路请求等等,当程式变复杂一不小就有可能忘记或是太晚解锁,造成整个程式非常慢甚至完全卡住,产生 死锁 问题
总结
这次用很简单的例子谈谈在Golang 中什么时候会遇到race condition 以及如何解决,因为要开一个goroutine 太简单了,所以有时候会不小心忘记考虑race condition,还好Go 有提供Race Detector 不用自己慢慢找XDD