好久沒寫博客了,不得不說go語言愛好者週刊是個寶貝,原本想隨便看看打發時間的,沒想到一會兒給了我久違的靈感。golang
go語言愛好者週刊78期出了一道很是有意思的題目。express
咱們來看看題目。先給出以下的代碼:c#
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) go fmt.Println(<-ch1) ch1 <- 5 time.Sleep(1 * time.Second) }
請問這串代碼的輸出是什麼。bash
我最早想到的是5,畢竟代碼很簡單,反應比較快的話代碼看完結果也就推斷出來了。app
然而題目給出的其中一個選項是輸出死鎖報錯,這個選項引發了個人好奇,因而我運行了一下:函數
$ go run a.go fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /tmp/a.go:10 +0x65 exit status 2
啊這。真的死鎖了。那麼我猜會不會和執行順序有關呢?因而我寫了個腳本運行1000次看看:google
#!/bin/bash for i in {0..1000} do go run a.go &> /dev/null if [ $? -eq 0 ] then echo 'success!' break fi done
結果天然是一次也沒成功,即便你改爲10000哪怕是1000000也是同樣的。執行順序帶來的影響咱們能夠排除了。lua
若是你仔細觀察的話,全部的報錯也都是同樣的:goroutine 1 [chan receive]:
,在這裏死鎖了。code
那麼會不會是由於使用了無緩衝chan的緣由呢?golang的內存模型規定了無緩衝chan的接受happens before發送操做,這會不會帶來影響呢(其實仔細想一想就很快排除了,happens before肯定的是內存的可見性,而不是指令執行的時間順序),因此我改了下代碼:內存
func main() { ch1 := make(chan int, 100) go fmt.Println(<-ch1) ch1 <- 5 time.Sleep(1 * time.Second) }
此次咱們使用了一個有容納100個元素的buff的channel,然而結果仍是沒有一點改變。
到這裏個人思路中斷了。
不過我還有google啊,因此我用「golang channel deadlock」爲關鍵詞搜索了一下,而後發現了一些有意思的結果。
那就是全部的chan的死鎖的代碼基本都能抽象成下面的形式:
func main() { ch1 := make(chan int) // 是否有buff無影響 _ = <-chan ch1 <- 5 }
這個代碼毫無疑問是會死鎖的,由於從chan接收值而chan裏是空的會致使當前goroutine進入等待,而當前goroutine不能繼續運行的話就永遠沒辦法向chan裏寫入值,死鎖就在這裏產生了。
在仔細觀察一下,你就會發現題目的代碼和這很像:
func main() { ch1 := make(chan int) go fmt.Println(<-ch1) ch1 <- 5 // sleep是爲了main routine不會過早退出 }
答案只有一個,<-ch1
發生在main goroutine裏了。
爲了佐證這一觀點,我有查閱了golang language spec,關於go語句有以下的描述:
The function value and parameters are evaluated as usual in the calling goroutine, but unlike with a regular call, program execution does not wait for the invoked function to complete.
函數和它的參數會像一般那樣在使用go語句的那個goroutine裏被執行,但不像常規的函數調用,程序不會同步等待這個函數執行完畢。
若是在看看有關求值的部分:
calls f with arguments a1, a2, … an. Except for one special case, arguments must be single-valued expressions assignable to the parameter types of F and are evaluated before the function is called.
用參數a1, a2等調用函數f,出了一個特例以外他們都必須是單值表達式,而且在函數運行前被求值。
上面說的特例是方法調用,方法的receiver會用特定的位置傳給method。
這樣事情的前因後果就清晰明瞭了,咱們來梳理一下。
假設咱們在main goroutine裏啓動一個子goroutine叫b,那麼實際上在main goroutine裏發生的事情是這樣的:
因此go fmt.Println(<-ch1)
裏的chan接收操做是在main goroutine裏執行的,所以死鎖是板上釘釘的事情。
若是改爲下面這樣,死鎖就不會發生:
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) go func() { fmt.Println(<-ch1) }() ch1 <- 5 time.Sleep(1 * time.Second) }
這是由於<-ch1
這回貨真價實地發生在了不一樣的goroutine裏,死鎖天然也不存在了。
這題很壞,壞就壞在fmt.Println(...)
這樣的形式容易讓人迷惑,覺得這個調用自己在新的goroutine裏執行,然而真正在新goroutine裏執行的倒是fmt.Println
內部的函數實現代碼,而不是fmt.Println(...)
這句,參數會在這以前就被求值。
那麼這能讓咱們學到什麼呢?答案是永遠也不要寫出題目裏那樣的代碼,對於chan的操做應該確保是在和執行go語句的goroutine不一樣的routine中運行的。
不過萬事不絕對,帶buff的chan會有些例外,固然這些之後有機會再說吧:P