Go語(yǔ)言單線程運(yùn)行也會(huì)有的并發(fā)問(wèn)題解析
Go單線程多goroutine訪問(wèn)一個(gè)map會(huì)遇到并發(fā)讀寫panic么?
一個(gè) Go 大佬群中嚴(yán)肅的討論了一個(gè)問(wèn)題:Go 程序單線程多 goroutine 訪問(wèn)一個(gè) map 會(huì)遇到并發(fā)讀寫 panic 么?
答案是肯定的,因?yàn)槌霈F(xiàn)了這個(gè)問(wèn)題所以大家才在群中討論。
為什么呢?因?yàn)閱尉€程意味著并行單元只有一個(gè)(多線程也可能并行單元只有一個(gè)),但是多 goroutine 意味著并發(fā)單元有多個(gè),如果并發(fā)單元同時(shí)執(zhí)行,即使是單線程,可能就會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題,除非這些 goroutine 是順序執(zhí)行的。
舉一個(gè)例子哈:
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)
}這段測(cè)試代碼是啟動(dòng) 10 個(gè) goroutine 對(duì)計(jì)數(shù)器加一,每個(gè) goroutine 負(fù)責(zé)加 10 萬(wàn)次。在我的 MBP m1 筆記本上,每次的結(jié)果都是 100 萬(wàn),符合期望。如果你運(yùn)行這段代碼,會(huì)發(fā)現(xiàn) goroutine 其實(shí)是一個(gè)一個(gè)串行執(zhí)行的(9->0->1->2->3->4->5->6->7->8,當(dāng)然可能在你的機(jī)器上不是這樣的),如果是串行執(zhí)行,不會(huì)有并發(fā)問(wèn)題:
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
插入runtime.Gosched()
為了制造點(diǎn)緊張氣氛,我將代碼改寫成下面這樣子,將counter++三條指令明顯寫成三條語(yǔ)句,并在中間插入runtime.Gosched(),故意給其它 goroutine 的執(zhí)行制造機(jī)會(huì):
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)
}運(yùn)行這段代碼,你就會(huì)明顯看到數(shù)據(jù)不一致的效果,即使是單個(gè)線程運(yùn)行 goroutine,也出現(xiàn)了數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題:
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
這個(gè)結(jié)果非常離譜,期望 100 萬(wàn),最后只有 10 萬(wàn)。
訪問(wèn)同一個(gè) map對(duì)象也有可能出現(xiàn)并發(fā)bug
因?yàn)閱蝹€(gè)線程運(yùn)行多個(gè) goroutine 會(huì)有數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題,所以訪問(wèn)同一個(gè) map 對(duì)象也有可能出現(xiàn)并發(fā) bug,比如下面的代碼,10 個(gè) goroutine 并發(fā)的寫同一個(gè) map:
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()
}大概率會(huì)出現(xiàn) panic:
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
以上就是Go語(yǔ)言單線程運(yùn)行的并發(fā)問(wèn)題解析的詳細(xì)內(nèi)容,更多關(guān)于Go單線程運(yùn)行并發(fā)問(wèn)題的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang服務(wù)的請(qǐng)求調(diào)度的實(shí)現(xiàn)
Golang服務(wù)請(qǐng)求調(diào)度是一種使用Go語(yǔ)言實(shí)現(xiàn)的服務(wù)請(qǐng)求管理方法,本文主要介紹了Golang服務(wù)的請(qǐng)求調(diào)度的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
Golang編程實(shí)現(xiàn)刪除字符串中出現(xiàn)次數(shù)最少字符的方法
這篇文章主要介紹了Golang編程實(shí)現(xiàn)刪除字符串中出現(xiàn)次數(shù)最少字符的方法,涉及Go語(yǔ)言字符串遍歷與運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-01-01
Go-家庭收支記賬軟件項(xiàng)目實(shí)現(xiàn)
這篇文章主要介紹了Go-家庭收支記賬軟件項(xiàng)目實(shí)現(xiàn),本文章內(nèi)容詳細(xì),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,需要的朋友可以參考下2023-01-01
Go語(yǔ)言通過(guò)chan進(jìn)行數(shù)據(jù)傳遞的方法詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言如何通過(guò)chan進(jìn)行數(shù)據(jù)傳遞的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06
Golang中自定義json序列化時(shí)間格式的示例代碼
Go語(yǔ)言作為一個(gè)由Google開發(fā),號(hào)稱互聯(lián)網(wǎng)的C語(yǔ)言的語(yǔ)言,自然也對(duì)JSON格式支持很好,下面這篇文章主要介紹了關(guān)于Golang中自定義json序列化時(shí)間格式的相關(guān)內(nèi)容,下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧2024-08-08

