很久沒寫博客了,不得不說go語言愛好者周刊是個寶貝,本來想隨便看看打發時間的,沒想到一下子給了我久違的靈感,
go語言愛好者周刊78期出了一道非常有意思的題目,
我們來看看題目,先給出如下的代碼:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
go fmt.Println(<-ch1)
ch1 <- 5
time.Sleep(1 * time.Second)
}
請問這串代碼的輸出是什么,
我最先想到的是5,畢竟代碼很簡單,反應比較快的話代碼看完結果也就推斷出來了,
然而題目給出的其中一個選項是輸出死鎖報錯,這個選項引起了我的好奇,于是我運行了一下:
$ 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次看看:
#!/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也是一樣的,執行順序帶來的影響我們可以排除了,
如果你仔細觀察的話,所有的報錯也都是一樣的:goroutine 1 [chan receive]:,在這里死鎖了,
那么會不會是因為使用了無緩沖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里發生的事情是這樣的:
- main goroutine執行到go陳述句
- go陳述句發現后面的函式運算式需要傳遞引數
- 于是被傳遞的引數在main goroutine里求值
- 新的goroutine b被創建,剛求值的引數傳遞給需要執行的函式(假設叫f),f在goroutine b中開始執行
- go陳述句結束,控制流程回到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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/250441.html
標籤:Go
下一篇:Go基礎及語法(二)
