golang 介紹以及踩坑之四

驕傲的殺生喵發表於2019-03-02
golang 介紹以及踩坑之四

這篇坑文來自最近的一件趣事。

我認識一位非常精通golang程式設計技巧的工程師。他/她經驗豐富,擅長各種解決工程問題的技法,對系統瞭解也極為深入。遇到golang的實戰問題,他/她往往可以一語中的,談笑間bug灰飛煙滅。

這麼一位值得尊敬的工程師,在別人問他golang的goroutine是個啥玩意的時候,他/她瞠目結舌,不知道該怎麼跟對方解釋好,居然說:“goroutine就有點像java的執行緒池啦。”
excuse me!這也太狗屁不通了吧!

所以我覺得,我來裝出一副我比他/她更懂的姿態,給大家科普一下什麼是goroutine。對goroutine瞭如指掌的同學請繞行。

那到底啥是goroutine捏?

要了解啥是goroutine,我們得先了解啥是coroutine。(不瞭解coroutine的同學請舉起腿來!—郭德綱)

coroutine也就是協程。

要了解什麼是協程,我們先得了解他的特殊形式:例程。

一個不built in支援協程的語言,寫出來的函式我們叫subroutine,或者叫例程。
subroutine A 呼叫 subroutine B
意味著在A的stack裡邊開創一片空間來作為B的stack。B裡邊可以繼續呼叫C,C裡邊可以繼續呼叫D… 要注意的是,所有後面被呼叫的傢伙都會共享A的執行緒開闢的stack空間。如果我們寫了一個呼叫巢狀特別複雜的函式,我們很有可能看見StackOverFlow! 當然如果我們寫一個A呼叫B,B裡邊再呼叫A這樣的子子孫孫無窮盡的函式呼叫,我們更容易碰到StackOverFlow!

例程基本講完了。

c/c++/java 不加上一些特殊庫的支援的話,我們寫的函式呼叫方式都是把對方當做例程來的。

而例程是協程的特殊形式!(重要的話要大黑粗)

我們可以很容易推斷出來,在一個執行緒裡邊,呼叫者例程一定是要等到被呼叫的例程執行完成並且返回才能繼續執行。比如:

public static void funcionA(){
  int resultFromB = functionB();
  System.out.println("B returned : " + resultFromB);
}
複製程式碼

而被呼叫的例程裡邊如果呼叫了呼叫者例程的話,也是重新開一個function stack來執行的。比如上面的栗子:如果functionB裡邊呼叫了functionA(好吧,我知道這麼寫的人是大sb),那麼另一個functionA的stack會被建立,然後執行。

但是coroutine呢?

var q := new queue

coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume

coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce
複製程式碼

coroutine produce和consume可以使用yield關鍵字把執行權推來推去。我們在這個例子裡邊可以直白的把yield理解為:我先歇歇

produce向q裡邊丟了東西,然後表示它要歇歇,讓consume幹會兒活。

consume用了q裡邊的東西,然後表示它要歇歇,讓produce幹會兒活。

produce和consume不是互為subroutine,互相的stack也是獨立的。

假如produce不使用yield關鍵字,直接呼叫consume,那就變成了subroutine的呼叫了。
所以我們說,subroutine是coroutine的特殊形式。

我們來看看goroutine

func main(){
ch:=make(chan int)
go routineA(ch)
go routineB(ch)
println("goroutines scheduled!")
<-ch
<-ch
}
func routineA(ch chan int){
 println("A executing!")
 ch<-1
}
func routineB(ch chan int){
 println("B executing!")
 ch<-2
}
複製程式碼

go這個關鍵字非常有用!他的意思是:

routineA 滾開,然後執行!

routineB 滾開,然後執行!

我們看到,main函式這個goroutine裡邊開啟了兩個新的goroutine,並且要求他們滾開去找個時間執行自己。我們可以斷言:”goroutines scheduled!”這行字將會先被輸出到console。而”A/B executing!“則會晚一些才輸出。
那麼問題來了,A和B啥時候才能得到執行機會呢?

答案:當正在執行的goroutine遇到系統IO(timer,channel read/write,file read/write…)的時候,go scheduler會切換出去看看是不是有別的goroutine可以執行一把,這個時候A和B就有機會了。實際上,這就是golang協程的概念。同時用少數的幾個執行緒來執行大量的goroutine協程,誰正在呼叫系統IO誰就歇著,讓別人用CPU。

所以如果我們用pprof看你的服務,可能發現有幾千條goroutine,但是真正執行的執行緒只有小貓兩三隻。

引申問題:假如我寫個不做任何系統IO的函式會怎麼樣?

func noIO(){
go routineA()
go routineB()
for {
 println("i will never stop!")
}
}
複製程式碼

go scheduler 專門對此作了處理。如果是早期的go版本,你將會看到大量的”i will never stop!”,並且發現routineA和B沒啥執行機會。現在go1.9會怎麼樣,各位童鞋不放舉起腿來自己試試看。

所以綜上所述:golang裡邊使用go 這個非常關鍵的關鍵字,來觸發協程排程。
相比python等語言對協程的支援,golang的支援是非常傻瓜友好的。比如python的

yield
await
run_until_complete
複製程式碼

分分鐘可以弄暈你。

希望這篇文章能對你有點小用處。向小白介紹goroutine的時候,我覺得可以這樣:
goroutine有點像是light weight的執行緒。一個真正的執行緒可以排程很多goroutine,不同的goroutine可以被掛載在不同執行緒裡邊去執行。這些都是自動的,對程式設計師很友好。

題外話,我們可以設定系統裡邊只有一條執行緒,所有的goroutine都在這一條執行緒上面跑。那麼我們可以省掉一個很噁心的東西:

對的,是sync.RWMutex.

相關文章