這篇坑文來自最近的一件趣事。
我認識一位非常精通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 queuecoroutine produce loop while q is not full create some new items add the items to q yield to consumecoroutine 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的
yieldawaitrun_until_complete
分分鐘可以弄暈你。
希望這篇文章能對你有點小用處。向小白介紹goroutine的時候,我覺得可以這樣:
goroutine有點像是light weight的線程。一個真正的線程可以調度很多goroutine,不同的goroutine可以被掛載在不同線程裡邊去執行。這些都是自動的,對程式員很友好。
題外話,我們可以設定系統裡邊只有一條線程,所有的goroutine都在這一條線程上面跑。那麼我們可以省掉一個很噁心的東西:
對的,是sync.RWMutex.