一个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#9, counter: 0 end task#9, counter: 100000 start task#0, counter: 100000 end task#0, counter: 200000 start task#1, counter: 200000 end task#1, counter: 300000 start task#2, counter: 300000 end task#2, counter: 400000 start task#3, counter: 400000 end task#3, counter: 500000 start task#4, counter: 500000 end task#4, counter: 600000 start task#5, counter: 600000 end task#5, counter: 700000 start task#6, counter: 700000 end task#6, counter: 800000 start task#7, counter: 800000 end task#7, counter: 900000 start task#8, counter: 900000 end task#8, counter: 1000000 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#9, counter: 0 start task#0, counter: 0 start task#1, counter: 0 start task#2, counter: 0 start task#3, counter: 0 start task#4, counter: 0 start task#5, counter: 0 start task#6, counter: 0 start task#7, counter: 0 start task#8, counter: 0 end task#9, counter: 100000 end task#1, counter: 100000 end task#3, counter: 100000 end task#2, counter: 100000 end task#5, counter: 100000 end task#0, counter: 100000 end task#4, counter: 100000 end task#6, counter: 100000 end task#7, counter: 100000 end task#8, counter: 100000 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