@[toc]
一、引入
观察如下程序
var val = 0
func main() {
go add()
go add()
time.Sleep(1 * time.Second)
fmt.Println(val)
}
func add() {
for i := 0; i < 100; i++ {
val++
}
}
在Java中,如果两个线程同一个变量加100次,那么结果是未知的,但是绝大数情况下val的值是小于200的。但是运行结果却是200(我试了好几次都是这个结果)
二、解释
这是修改后的程序以及运行结果,似乎找到了原因。因为add()运行太快了,第一个协程运行完成后第二个协程才开始运行。加上Sleep后,两个协程能交替执行,才会出现数据竞争。
三、再深一步
- 为什么数据竞争这么“严重”
理论上,5ms对于cpu来说是很长的,两个协程同时修改val概率没有这么大,所以结果在100~200直接,会更靠近200些。 - 上面是公司的笔记本运行结果,而在我的台式机上结果还是200,这是为什么
第一个问题,涉及到内存同步,参考链接:曹政谈重排。两个协程的操作大部分只写到各自的store buffer中,最终内存同步时,才会有大量自增是无效的。
第二个问题,我开始认为我的电脑是单核的,但是看配置却是
然后,我怀疑是我的Go环境没有配置正确(使用的默认值)
GOMAXPROCS:在Go 1.5版本之前,GOMAXPROCS参数的值默认是1
我没有设置GOMAXPROCS,而Go的版本是1.16,GOMAXPROCS没有设置的话,有几个核就会使用几个核,并且加上如下代码,结果显示并不是单核运行
num := runtime.NumCPU()
fmt.Println("使用cpu数量", num) // 使用cpu数量 12
然后,我又怀疑两个协程没有交替运行,于是我又改进了程序,将val改变的中间值,以及哪个协程修改打印了出来
var val = 0
func main() {
num := runtime.NumCPU()
fmt.Println("使用cpu数量", num)
go add("A")
go add("B")
time.Sleep(1 * time.Second)
fmt.Println("val的最终结果", val)
}
func add(proc string) {
for i := 0; i < 100; i++ {
val++
fmt.Printf("execute process[%s] and val is %d\n", proc, val)
time.Sleep(5 * time.Millisecond)
}
}
但是结果如下
证明两个协程是交替运行的,我又迷惑了
四、结尾
感谢rustyx的解惑data-race question
上面程序有两个需要改进的地方
- 主协程sleep时间太短了,笔记本上运行结果118可能是因为add()协程没有运行完。
- val自增的次数(100)太少了,一个协程先跑完,另一个还没开始运行
于是,我又改进了代码
var val = 0
func main() {
num := runtime.NumCPU()
fmt.Println("使用cpu数量", num)
var wg = sync.WaitGroup{}
wg.Add(2)
go add("A", &wg)
go add("B", &wg)
wg.Wait()
fmt.Println("val的最终结果", val)
}
func add(proc string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
val++
}
}
这是笔记本上运行的一次结果
下面是台式机上运行的结果
终于,可以看到发生数据竞争了。当然,更多的运行结果还是2000
网友评论