并发编程常见问题及go的锁,条件变量及原子操作
竞态条件
第一个版本,不带锁,起10个协程并发的修改Counter,可以看到结果每次都不一样,并且没有规律
1 | package main |
结果如下:
1 | goroutine 5:1000 |
c.count++的操作是读取,修改,写入,如果两个goroutine同时读取c.count,假设值为100,则都会修改为101,并且第二个goroutine写入101后覆盖掉第一个goroutine写入的101.
加锁版本如下:
1 | type Counter struct { |
结果如下:
1 | goroutine 5:2600 |
可以看到,最终结果是固定的10000
上边的竞态条件比较简单,我们可以直接使用原子操作,如下:
1 | type Counter struct { |
由于atomic包只有AddInt64和AddInt32方法,因此修改count的类型为int64,结果如下:
1 | goroutine 2:1000 |
结果也是固定为10000
go有内置的竞态检测机制(当两个goroutine同时访问同一个变量,并且至少一个是写的时候就会发生竞态),如下:
1 | localhost:copywriter.io didi$ go run -race concurrency/origin.go |
使用go的benchmark测试一下sync.Mutex和atomic的性能:
1 | localhost:concurrency didi$ go test -bench=. |
Mutex是Atomic性能的1/3
乱序执行
1 | var a, b int |
如上代码,可能会打印出2,0.原因为编译器或者CPU可能会乱序执行 a=1和b=2两个语句,当执行g()的时候b已经更新为2,但是a仍然为1
可见性
1 | var a string |
上述代码首先不能保证会打印出”hello,world”,因为可能会乱序.更糟糕的是,
main函数可能会永远无法退出,因为done的可见性不能保证.虽然在另一个协程中更新了done,但main函数中不能保证会读取到正确的done
使用竞态检测器检测结果如下:
1 | localhost:concurrency didi$ go run -race condition.go |
使用条件变量改写如下:
1 | package main |
检测器输出结果为:
1 | localhost:concurrency didi$ go run -race condition1.go |
压测前后两个版本,比较增加锁之后的性能:
1 | BenchmarkCondition1-4 2000000 624 ns/op |
将条件变量改为channel,如下:
1 | package main |
压测结果如下:
1 | BenchmarkCondition1-4 2000000 612 ns/op |
condition1为加条件变量版本,2为有竞态问题的版本,3为channel版本