七叶笔记 » golang编程 » 用一个小例子谈谈Golang 中的Race Condition

用一个小例子谈谈Golang 中的Race Condition

Goroutine 是Go 最重要的特性之一,它可以让开发者轻易做到并发(concurrency),而且他的非常轻量,所以一次开一大堆goroutine 也不会有什么问题。

但如果在使用goroutine 时没有考虑到race condition,那可能就会导致不正确的结果,这篇文章会用一个简单的小例子,谈谈在什么情况可能会遇到race condition,以及如何发现、解决它。

Race condition (竞争危害)

来看看这次要讲解的例子,分成三个步骤

  1. 先把a 的初始值设为 0
  2. 开三个goroutine 共做了三次 a++
  3. 最后用channel 等待三个goroutine 完成

没意外的话最后应该会得到a = 3,结果也确实如此

那如果把次数改成 一万次 呢?

理论上要得到a = 10000,实际跑了却会得到a = 9903之类的结果(如果你有多核CPU的话),但我们确实开了一万个goroutine也做了一万次a++,为什么结果会不对呢,因为在a++的过程中发生了race condition

为什么a++ 会发生race condition

当你写了a++时电脑实际上做了三件事:

  1. CPU 把a 的值取出来
  2. 把刚刚取得的值加 1
  3. 把运算的结果存回变数 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

相关文章