一个Go大佬群中严肃的讨论了一个问题:Go程序单线程多goroutine访问一个map会遇到并发读写panic么?
答案是肯定的,因为出现了这个问题所以大家才在群中讨论。
为什么呢?因为单线程意味着并行单元只有一个(多线程也可能并行单元只有一个),但是多goroutine意味着并发单元有多个,如果并发单元同时执行,即使是单线程,可能就会产生数据竞争的问题,除非这些goroutine是顺序执行的。
举一个例子哈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| func TestCounter() { runtime.GOMAXPROCS(1) var counter int var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { i := i go func() { fmt.Printf("start task#%d, counter: %d\n", i, counter) for j := 0; j < 10_0000; j++ { counter++ } fmt.Printf("end task#%d, counter: %d\n", i, counter) wg.Done() }() } wg.Wait() fmt.Println(counter) }
|
这段测试代码是启动10个goroutine对计数器加一,每个goroutine负责加10万次。在我的MBP m1笔记本上,每次的结果都是100万,符合期望。如果你运行这段代码,会发现goroutine其实是一个一个串行执行的(9->0->1->2->3->4->5->6->7->8,当然可能在你的机器上不是这样的),如果是串行执行,不会有并发问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| start task end task start task end task start task end task start task end task start task end task start task end task start task end task start task end task start task end task start task end task 1000000
|
为了制造点紧张气氛,我将代码改写成下面这样子,将counter++
三条指令明显写成三条语句,并在中间插入runtime.Gosched()
,故意给其它goroutine的执行制造机会:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func TestCounter2() { runtime.GOMAXPROCS(1) var counter int var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { i := i go func() { fmt.Printf("start task#%d, counter: %d\n", i, counter) for j := 0; j < 10_0000; j++ { temp := counter runtime.Gosched() temp = temp + 1 counter = temp } fmt.Printf("end task#%d, counter: %d\n", i, counter) wg.Done() }() } wg.Wait() fmt.Println(counter) }
|
运行这段代码,你就会明显看到数据不一致的效果,即使是单个线程运行goroutine,也出现了数据竞争的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| start task start task start task start task start task start task start task start task start task start task end task end task end task end task end task end task end task end task end task end task 100000
|
这个结果非常离谱,期望100万,最后只有10万。
因为单个线程运行多个goroutine会有数据竞争的问题,所以访问同一个map对象也有可能出现并发bug,比如下面的代码,10个goroutine并发的写同一个map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func TestMap() { var m = make(map[int]int) var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { i := i go func() { fmt.Printf("start map task#%d, m: %v\n", i, len(m)) for j := 0; j < 10_0000; j++ { m[j] = i*10_0000 + j } fmt.Printf("end map task#%d, m: %v\n", i, len(m)) wg.Done() }() } wg.Wait() }
|
大概率会出现panic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| start map task#9, m: 0 start map task#0, m: 49152 fatal error: concurrent map writes goroutine 41 [running]: main.TestMap.func1() /Users/chaoyuepan/study/single_thread/main.go:72 +0xcc created by main.TestMap in goroutine 1 /Users/chaoyuepan/study/single_thread/main.go:69 +0x4c goroutine 1 [semacquire]: sync.runtime_Semacquire(0x140000021a0?) /usr/local/go/src/runtime/sema.go:62 +0x2c sync.(*WaitGroup).Wait(0x1400000e1d0) /usr/local/go/src/sync/waitgroup.go:116 +0x74 main.TestMap() /Users/chaoyuepan/study/single_thread/main.go:79 +0xb8 main.main() /Users/chaoyuepan/study/single_thread/main.go:15 +0x2c
|