為了學(xué)習(xí) Go 通過 channel 實(shí)現(xiàn)同步的機(jī)制,我寫了以下代碼來試驗(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
}
}
上面的代碼很簡單,就是有兩個 goroutine 共享counter這個變量進(jìn)行讀寫操作,counter最終的輸出值應(yīng)該是4。為了避免出現(xiàn)不同步的情況,把counter放入一個無緩沖的 channel 中,通過這個 channel 在兩個 goroutine 之間傳遞counter。
運(yùn)行之后程序報(bào)錯:
fatal error: all goroutines are asleep - deadlock!
這意味著所有 goroutine 都被阻塞了,整個程序進(jìn)入死鎖狀態(tài),可是為什么?
第一個坑
一開始我是百思不得其解,于是就在 Stack Overflow 上求助,有位哥們一語道破天機(jī)。他解釋道,上面這段程序的執(zhí)行流程是這樣的:

原來是沒有 goroutine 在最后從 channel 中取出counter,而這又是個無緩沖的 channel,所有 goroutine 都被阻塞了,自然也就死鎖了。
解決方法
很簡單,在main中把fmt.Println("Final Counter:", counter)修改為:
fmt.Println("Final Counter:", <-ch)
也就是讓主 goroutine 負(fù)責(zé)取出最終的counter。
第二個坑
本來以為這樣就行了,沒想到運(yùn)行之后還是死鎖。我 debug 了很久之后終于找出了第二個坑。
程序中使用了sync.WaitGroup這個類型來阻止主 goroutine 在其他子 goroutine 之前終止,也就是不讓main函數(shù)提前退出。wg.Add(2)就是告訴main要等兩個 goroutine 終止之后才能退出,而在 goroutine 中則是通過wg.Done()來通知main函數(shù)某個 goroutine 已終止。
wg.Done()前面加上了一個defer,也就是要等到incCounter中其他代碼都執(zhí)行完了才會調(diào)用wg.Done()。這就是問題所在,像上面那張圖展示的那樣,goroutine 2 把counter放入 channel 中,等著主 goroutine 取出;但在main函數(shù)中, fmt.Println("Final Counter:", <-ch)之前有一句wg.Wait(),也就是說要等wg.Wait()方法退出了才能執(zhí)行下一句。然而此時這個方法并沒有退出,因?yàn)?goroutine 2 還在被ch <- counter阻塞中,也就沒法退出循環(huán)體,那么也就沒法執(zhí)行wg.Done()!程序也就又死鎖了。
解決
必須手動調(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,兩個 goroutine 實(shí)現(xiàn)了同步。