這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
目錄 [−]
- go 語句
- 深入go語句
- goroutine是什麼
- goroutine的調度
本章介紹 go語句、goroutine調度。
go 語句
go語句用來產生一個新的goroutine,並執行一個函數,它的使用非常簡單,就是在函數調用或者方法調用的前面加上go關鍵字即可。
函數可以是已有函數、匿名函數、方法等,注意匿名方法(方法字面量)不要忘記調用。
123456789101112131415 |
func foo(i int) int {return i * i}……go foo(10)go func() {}()go os.Open("./test.txt")buf := bytes.NewBufferString("hello world")go buf.ReadString(0) |
深入go語句
看下面一段代碼,你覺得會輸出什麼:
123456789101112 |
package mainimport "fmt"func main() {for i := 0; i < 3; i++ {go func() {fmt.Println(i)}()}} |
有的人說輸出"0 1 2",有的人說輸出"3 3 3"。
但實際上什麼都沒有輸出。這是因為main goroutine馬上就執行完了,它不會等待產生的goroutine的執行。
Program execution begins by initializing the main package and then invoking the function main. When that function invocation returns, the program exits. It does not wait for other (non-main) goroutines to complete.
你可以增加下面一行,等待所有的goroutine
執行完:
因為select語句會被阻塞,所以前面產生的所有的goroutine會被執行。
你可能會發現程式最後會出下面一個錯誤資訊:
1 |
fatal error: all goroutines are asleep - deadlock! |
它的意思是所有的goroutine都已經執行完了,你的select還在那裡阻塞著,不會有case等你執行的,所以有死結的可能。Go強制殺死了這個等待,並拋出了一個錯誤。因此你可以忽略這個錯誤,它對我們前面的程式執行沒有影響。
如果你不想看到這個錯誤,你可以使用sync.WaitGroup
,或者像其它語言中的處理方法一樣,從命令列讀取一個值造成main goroutine阻塞,抑或加一行time.Sleep讓main goroutine休眠較長的一個時間也可以:
1 |
os.Stdin.Read(make([]byte, 1)) |
那麼,加上上面一行,會輸出什麼?
答案是 "3 3 3",為什麼呢?
這是因為對於closure的情況(閉包closure的概念在很多語言中都有使用。在Go中,可以簡單的認為匿名函數保持對外部變數的引用),for迴圈的每次迭代都會使用相同的變數i,這樣每個goroutine都持有對相同的變數的引用,因為main gororutine 很快就執行了, 三個goroutine還沒來得及執行,等它們執行的時候,i已經等於 2了,所以它們都列印出2來。
我們可以稍微修改一下,讓main goroutine不要執行那麼快,每次迭代暫停1秒:
1234567 |
for i := 0; i < 3; i++ {go func() {fmt.Println(i)}()time.Sleep(1 * time.Second)} |
這段代碼輸出的結果為"0 1 2"。因為在main goroutine 暫停時候, 產生的go routine有機會執行。
但是我們無法精確控制goroutine的執行,如果期望輸出結果總是使用當前的迭代的值,可以改造成下面的樣子:
12345 |
for i := 0; i < 3; i++ {go func(v int) {fmt.Println(v)}(i)} |
輸出結果為"2 0 1" (goroutine的執行順序有可能不同,但是如果你看到最後一節的分析,這個執行順序也能講得通,最後一個輸出2的goroutine作為runnext優先順序最高,輸出1的goroutine本來在runnext的位置,不幸被擠掉了,放在了本地隊列的隊尾)。
如果你不想對匿名函數進行改造的話,也可以像下面的這樣,產生一個局部變數:
123456 |
for i := 0; i < 3; i++ {i := igo func() {fmt.Println(i)}()} |
輸出 "2 0 1", 注意我們使用一個同名的局部變數shadow了迭代的變數i。
參考:
- https://golang.org/doc/faq#closures_and_goroutines
goroutine是什麼
goroutine是Go語言專屬的概念。
並發和多線程編程總是被認為很困難,多少是由於它們的實現,對於線程和並發訪問的控制很複雜。 Go語言並發的基礎是goroutine和channel。
這些概念來源於著名電腦科學家C.A.R.Hoare的Communication Sequential Process (簡稱CSP)。
在該語言中,一個並發系統由若干並行啟動並執行順序進程組成,每個進程不能對其他進程的變數賦值。進程之間只能通過 一對通訊原語實現協作:Q->x表示從進程Q輸入一個值到變數x中;P<-e表示把運算式e的值發送給進程P。當P進程執行Q->x, 同時Q進程執行P<-e時,發生通訊,e的值從Q進程傳送給P進程的變數x。
Occam和Erlang基於CSP的理論實現的並行存取模型。
Go也借鑒了CSP的理論,但又有所不同,最大的不同是Go顯示地使用channel, channel在Go中是第一類的對象,goroutine通訊完全通過通過channel實現的。
CSP模型中訊息的分發是即時和同步的,Go的Channel則不同,訊息會緩衝在Channel中。
我看到的一個有趣的項目是使用Go語言實現Hoare論文中的例子,有興趣的朋友可以仔細觀看,csp。
幸運地是,這些實現的細節對於Go語言的學習和應用來說不是必須的,對於語言的設計者來說,倒是值得比較和研究和出論文。
但是,對於開發人員來說,至少應該明白goroutine和線程的不同,為什麼一個Go應用可以存在成千上萬個goroutine為線程確不行。
goroutine vs thread
對於線程來講,Java的線程是最有名了。我們從三個方面進行比較:
1、記憶體佔用
goroutine並不需要太多太多的記憶體佔用,初始只需2kB的棧空間即可(自Go 1.4起),按照需要可以增長。
線程初始1MB,並且會分配一個防護頁(guard page)。
在使用Java程式開發伺服器的過程中經常會遇到request per thread的問題,如果為每個請求都分配一個線程的話,大並發的情況下伺服器很快就死掉,因為記憶體不夠了,所以很多Java架構比如Netty都會使用線程池來處理請求,而不會讓線程任意增長。
而使用goroutine則沒有這個問題,你頁可以看到官方的net/http庫就是使用request per goroutine這種模式進行處理的,記憶體佔用不會是問題。
2、對象的建立和銷毀
線程的建立和銷毀肯定有花費,因為需要從OS中請求/返還資源。
而goroutine的建立和銷毀花費很少,因為它是使用者態的操作。並且Go語言也不提供goroutine的手工管理。
3、切換時間
當線程阻塞時,其它的線程進可能被執行,這叫做線程的切換。切換的時候,調度器需要儲存當前阻塞的線程的狀態,恢複要執行的線程狀態,包括所有的寄存器,16個通用寄存器、程式計數器、棧指標、段寄存器、16個XMM寄存器、FP副處理器、16個 AVX寄存器、所有的MSR等等。
goroutine的儲存和恢複只需要三個寄存器:程式計數器、棧指標和DX寄存器。因為goroutine之間共用堆空間,不共用棧空間,所以只需把goroutine的棧指標和程式執行到那裡的資訊儲存和恢複即可,花費很低。
通過上面三個方面的分析,可以看到goroutine比線程有更多的優勢。實際上Go使用少量線程來執行這些goroutine,通過GOMAXPROCS環境變數可以控制有多少線程可以並發執行使用者態的代碼。由於系統調用而被阻塞的線程不受這個變數的限制。以前版本的Go中這個變數為1,自Go 1.5後它的預設值為CPU的核心數。
進程擁有自己獨立的堆和棧,既不共用堆,亦不共用棧,進程由作業系統調度。
線程擁有自己獨立的棧和共用的堆,共用堆,不共用棧,線程亦由作業系統調度(標準線程是的)。
協程和線程一樣共用堆,不共用棧,協程由程式員在協程的代碼裡顯示調度。
goroutine vs coroutine
兩個類似,都是共用堆,不共用棧,切換的時候需要儲存和恢複棧資訊。
但是coroutine(協程)需要顯示地控制coroutine的轉換,程式員需要在切換的地方調用yield
讓度當前的coroutine的執行,這樣其它coroutine才有可能在這個線程中執行,等暫停coroutine恢複執行的時候,它會接著上次暫停地方繼續執行,而不像普通的函數從頭開始執行。 看一段lua的coroutine代碼:
123456789101112131415161718 |
function foo (a) print("foo", a) return coroutine.yield(2*a) end co = coroutine.create(function (a,b) print("co-body", a, b) local r = foo(a+1) print("co-body", r) local r, s = coroutine.yield(a+b, a-b) print("co-body", r, s) return b, "end" end) print("main", coroutine.resume(co, 1, 10)) print("main", coroutine.resume(co, "r")) print("main", coroutine.resume(co, "x", "y")) print("main", coroutine.resume(co, "x", "y")) |
輸出:
123456789 |
co-body 1 10foo 2main true 4co-body rmain true 11 -9co-body x ymain true 10 endmain false cannot resume dead coroutine |
可以看到coroutine切換都是通過代碼中的yield觸發的。
goroutine也是由一組線程執行,也會暫停,也會繼續執行,但是這個控制不是程式員實現安排好的,它是由go運行時後台控制的。goroutine的調度不能手工的執行,這是和coroutine最大的區別。當goroutine阻塞的時候,就有可能讓度出線程以便其它goroutine執行,以下幾種情況goroutine可能暫停自己的運行:
- 調用runtime.Gosched()將當前goroutine放入到全域隊列
- 調用runtime.Goexit,終止G任務
- 網路讀取
- sleep
- channel操作
- 調用sync包中的對象進行阻塞
- 其它gouroutine被阻塞的情況,比如io讀取,空無限迴圈,長時間佔用線程執行的goroutine
利用goroutine和channel也可以實現cororutine,比如下面的代碼:
12345678910111213 |
func f(yield chan string) {yield <- "one"yield <- "two"yield <- "three"}func main() {co := make(chan string)go f(co)log.Println(<-co) // onelog.Println(<-co) // twolog.Println(<-co) // three} |
參考
- https://talks.golang.org/2012/concurrency.slide
- https://talks.golang.org/2012/waza.slide
- http://blog.nindalf.com/how-goroutines-work/
- http://stackoverflow.com/questions/18058164/is-a-go-goroutine-a-coroutine
- http://www.golangpatterns.info/concurrency/coroutines
- https://golang.org/doc/faq#goroutines
- https://blog.golang.org/share-memory-by-communicating
- http://stackoverflow.com/questions/32651557/golang-main-difference-from-csp-language-by-hoare
- http://www.informit.com/articles/printerfriendly/1768317
- https://en.wikipedia.org/wiki/Coroutine
- http://www.jianshu.com/p/36e246c6153d
- https://github.com/golang/go/issues/4056
- http://stackoverflow.com/questions/28354141/c-code-and-goroutine-scheduling
goroutine的調度
goroutine調度(Scheduling)的文章網上非常多了,而且分析的都很深入。本文重點的介紹其中的一些細節。
goroutine調度器有三個重要的資料結構,都是以單字母命名: G、P、M,因為Golang以及實現了自舉,所以絕大部分的代碼都是由Go本身實現的,少部分的以彙編實現,因為你已經由Go的基礎知識了,所以你可以查看這些實現的代碼不會感到特別困難。
- M代表系統線程(Machine),由作業系統管理。
- G代表goroutine,包括棧/指令指標以及其它對調度goroutine有用的資訊。
- P代表處理器(processor),注意不是CPU處理器,而是調度處理器,包含調度的上下文。
這三個個對象的資料結構定義在Go原始碼的src/runtime/runtime2.go中定義,另外還包括一個很重要的資料結構schedt。
P必須和M組合起來執行G,但是兩者也並不是完全1:1對應,通常情況下P的數量固定和CPU的核心數一樣(GOMAXPROCS參數),M則是按需建立,比如當M因為陷入系統調用而長時間阻塞的時候,P就會被監控線程搶回,去建立或者喚醒另一個M去執行,因此M的數量會增加,系統中可能存在一些阻塞的M。
當一個G被建立的時候,它可能被放入到一個P的本地隊列或者全域隊列中:
由於goroutine的執行的時間不會一樣,goroutine不可能均勻地分布在所有的P的本地隊列中,如果其中的一個P執行地很快,它的隊列中沒有其它的gouroutine需要執行了,它就會從全域隊列中拿一批goroutine過來。
如果全域隊列中也沒有要執行的goroutine,那麼這個P可能要從其它的P中“偷”一些goroutine過來。
這樣設計的目的就是不要讓一部分P忙的要死,另外一部分P確很清閑,這是一個balance的過程。
編譯器會將"go func(……){}(……)"翻譯成"newproc"調用,這個方法在runtime/proc.go中定義:
1234567891011121314 |
// Create a new g running fn with siz bytes of arguments.// Put it on the queue of g's waiting to run.// The compiler turns a go statement into a call to this.// Cannot split the stack because it assumes that the arguments// are available sequentially after &fn; they would not be// copied if a stack split occurred.//go:nosplitfunc newproc(siz int32, fn *funcval) {argp := add(unsafe.Pointer(&fn), sys.PtrSize)pc := getcallerpc(unsafe.Pointer(&siz))systemstack(func() {newproc1(fn, (*uint8)(argp), siz, 0, pc)})} |
它的建立G的主要邏輯在newproc1中實現,並調用runqput
將建立的G放入到隊列中。注意G是可以重用的,如果有重用的G,則選擇一個,否則建立一個新的,而且它也有本地複用鏈表和全域複用鏈表。
runqput
首先嘗試將G放入到P本地隊列的本地隊列中,而且在不設定"-race"的情況下,可能會嘗試將這個G放在p.runnext中,作為下一個優先處理的G,而原先的runnext放回隊尾。如果本地隊列已滿,則放入到全域隊列中,而且還會將本地隊列的一部分放入到全域隊列中。
任務隊列的優先順序分三種:P.runnext、P.runq和全域的Schedt.runq。
schedule
方法用來實現goroutine的調用,你可以在proc.go檔案中搜尋對它的調用。
如果你瀏覽schedule()方法的實現,可以看到每隔一定時間,會先嘗試從全域隊列中擷取g去執行,這樣就避免全域隊列中的g沒機會執行。
然後嘗試本地隊列中擷取g, 依照優先順序選擇g,先是P.runnext,然後從隊列的頭部依次擷取。
如果本地隊列沒有g,則調用findrunnable
方法從其它地方擷取,這是一個block方法,直到有g擷取到。
findrunnable首先從本地隊列擷取(runqget方法),然後從全域隊列擷取(globrunqget),然後檢查netpoll的goroutine,
如果還沒有,隨機播放一個P,偷一些任務過來(runqsteal方法,如果“餓”的厲害,連別人的runnext都偷過來)。
具體的擷取過程你可以查看每個選擇的方法。
你可以通過schedtrace調試參數查看Go調度的細節:
1 |
GOMAXPROCS=1 GODEBUG=schedtrace=1000 ./example |
詳細的文章可以查看我翻譯的William Kennedy的Scheduler Tracing In Go
參考
- https://golang.org/pkg/runtime/
- http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf
- https://morsmachine.dk/go-scheduler
- http://studygolang.com/articles/6070
- http://www.slideshare.net/matthewrdale/demystifying-the-go-scheduler
- https://github.com/qyuhen/book/blob/master/Go%201.5%20%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.pdf
- https://www.goinggo.net/2015/02/scheduler-tracing-in-go.html
- https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.1.html
- http://dave.cheney.net/2015/08/08/performance-without-the-event-loop