為了學(xué)習(xí) Go 通過(guò) channel 實(shí)現(xiàn)同步的機(jī)制弦叶,我寫(xiě)了以下代碼來(lái)試驗(yàn):
import (
"fmt"
"sync"
)
?
var (
counter int64
wg sync.WaitGroup
)
?
func main() {
ch := make(chan int64)
?
wg.Add(2)
?
go incCounter(ch)
go incCounter(ch)
ch <- counter
?
// wait until two goroutines exit
wg.Wait()
?
// expected value is 4
fmt.Println("Final Counter:", counter)
}
func incCounter(ch chan int64) {
defer wg.Done()
?
for count := 0; count < 2; count++ {
// receive data from channel
value := <-ch
?
value++
counter = value
?
// send data to channel
ch <- counter
}
}
上面的代碼很簡(jiǎn)單,就是有兩個(gè) goroutine 共享counter
這個(gè)變量進(jìn)行讀寫(xiě)操作,counter
最終的輸出值應(yīng)該是4。為了避免出現(xiàn)不同步的情況续崖,把counter
放入一個(gè)無(wú)緩沖的 channel 中,通過(guò)這個(gè) channel 在兩個(gè) goroutine 之間傳遞counter
团搞。
運(yùn)行之后程序報(bào)錯(cuò):
fatal error: all goroutines are asleep - deadlock!
這意味著所有 goroutine 都被阻塞了严望,整個(gè)程序進(jìn)入死鎖狀態(tài),可是為什么逻恐?
第一個(gè)坑
一開(kāi)始我是百思不得其解像吻,于是就在 Stack Overflow 上求助,有位哥們一語(yǔ)道破天機(jī)复隆。他解釋道拨匆,上面這段程序的執(zhí)行流程是這樣的:
原來(lái)是沒(méi)有 goroutine 在最后從 channel 中取出counter
,而這又是個(gè)無(wú)緩沖的 channel挽拂,所有 goroutine 都被阻塞了惭每,自然也就死鎖了。
解決方法
很簡(jiǎn)單亏栈,在main
中把fmt.Println("Final Counter:", counter)
修改為:
fmt.Println("Final Counter:", <-ch)
也就是讓主 goroutine 負(fù)責(zé)取出最終的counter
台腥。
第二個(gè)坑
本來(lái)以為這樣就行了,沒(méi)想到運(yùn)行之后還是死鎖绒北。我 debug 了很久之后終于找出了第二個(gè)坑黎侈。
程序中使用了sync.WaitGroup
這個(gè)類(lèi)型來(lái)阻止主 goroutine 在其他子 goroutine 之前終止,也就是不讓main
函數(shù)提前退出闷游。wg.Add(2)
就是告訴main
要等兩個(gè) goroutine 終止之后才能退出峻汉,而在 goroutine 中則是通過(guò)wg.Done()
來(lái)通知main
函數(shù)某個(gè) goroutine 已終止。
wg.Done()
前面加上了一個(gè)defer
脐往,也就是要等到incCounter
中其他代碼都執(zhí)行完了才會(huì)調(diào)用wg.Done()
休吠。這就是問(wèn)題所在,像上面那張圖展示的那樣业簿,goroutine 2 把counter
放入 channel 中瘤礁,等著主 goroutine 取出;但在main
函數(shù)中辖源, fmt.Println("Final Counter:", <-ch)
之前有一句wg.Wait()
蔚携,也就是說(shuō)要等wg.Wait()
方法退出了才能執(zhí)行下一句。然而此時(shí)這個(gè)方法并沒(méi)有退出克饶,因?yàn)?goroutine 2 還在被ch <- counter
阻塞中酝蜒,也就沒(méi)法退出循環(huán)體,那么也就沒(méi)法執(zhí)行wg.Done()
矾湃!程序也就又死鎖了亡脑。
解決
必須手動(dòng)調(diào)用wg.Done()
而不能依賴defer
的延遲調(diào)用機(jī)制:
func incCounter(ch chan int64) {
for count := 0; count < 2; count++ {
// receive data from channel
value := <-ch
?
value++
counter = value
?
// if this goroutine has incremented counter twice,
// it will exit
if count == 1 {
wg.Done()
}
?
// send data to channel
ch <- counter
}
}
至此,程序終于可以正常運(yùn)行,counter
最后的輸出為4霉咨,兩個(gè) goroutine 實(shí)現(xiàn)了同步蛙紫。