Goroutine 是如何運行的

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Go 語言中,沒有線程,只有 goroutine,這也是 Go 語言原生支持高併發的關鍵。 goroutine 是 Go 語言對協程的實現。goroutine 非常輕量級,一般只有幾 Kb 的大小,而一個線程最小都有 1 M。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"goroutine 本身只是一個數據結構,真正讓 goroutine 運行起來的是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"調度器","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1. 爲什麼需要一個調度器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在計算機上運行的程序最終都是需要 CPU 去執行,協程只是運行在操作系統的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"用戶態","attrs":{}},{"type":"text","text":"。協程真正的執行依然需要依靠操作系統","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"內核態","attrs":{}},{"type":"text","text":"的線程去執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作系統並不知道協程的存在,會把協程當做普通的程序來執行。既然協程是爲了提高程序的執行效率,那麼一個理想的情況是一個線程上可以執行多個協程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果一個協程對於一個線程,那就相當於協程的創建和運行還是由內核態來執行,這樣的代價有點高。但如果一個線程上可以運行多個協程,如果其中的一個協程發生了阻塞,那麼其他的協程就都無法執行了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以理想的情況是協程是線程的關係是 m:n,這樣就可以克服 m:1 和 1:1 的缺點。但 m:n 的情況最爲複雜,需要自己來實現協程在多個線程的調度,充分利用計算機的多核能力,再配合協程的輕量級的特性,實現程序的高併發。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Go 的實現中,goroutine 與內核態線程的對應關係就是是 m:n,所以就需要自己實現一個協程的調度器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/96/96b8ea7b06e702ba80791b94c69d677f.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2. 調度器的結構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go 調度器從最開始到現在也經歷了不斷的演進,最初的那個版本已經被放棄,目前使用的版本是在 2012 重新設計的,然後沿用至今。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在用的這個調度器也被稱之爲 GMP 模型,3 個字母分表代表一個關鍵部件的名稱:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"G:表示 goroutine,就是代表待執行的協程","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"M:M 表示的是內核態的線程,goroutine 真正的執行需要依賴 M","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"P:P 是調度器的核心,它會把 G 調度到合適的 M 上去執行,讓 G 的執行儘可能快的完成","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/80aaa9a9842e1b47c8d772e8392caf12.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果 M,也就是線程如果想要運行任務,就需要去獲取一個 P,然後從 P 的任務隊列中獲取 goroutine 來執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 P 上,會有一個正在 M 上執行的 G,但是同時也會維護一個本地的隊列,裏面都是待執行的 G,其中 P 的數量由 GOMAXPROCS 環境變量或者 runtime.GOMAXPROCS() 來決定,這表示在同一時間,只有 GOMAXPROCS 數量個 goroutine 在執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"P 與 M 的數量沒有固定的關係,如果當前的 M 阻塞了,P 就會去創建或者切換到另一個 M 上。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3. 調度器是如何運作的","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在介紹完 GMP 的結構之後,我們再來看一下 GMP 調度器是如何運行起來的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Go 語言中,我們創建一個 goroutine 非常簡單,只需要使用 go 關鍵字:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"go func() {\n fmt.Println(\"New goroutine\")\n}()\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣就會創建上面所說的一個 G,然後放進調度器中開始調度。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3e/3e8c49e3942b0c89b9123eb49c83337b.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個 G 在被創建之後,都會被優先放入到本地隊列中,如果本地隊列已經滿了,就會被放入到全局隊列中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後每個 M 就開始執行 P 的本地隊列中的 G,如果某個 M 把任務都執行完成之後,然後就會去去全局隊列中拿 G,這裏需要注意,每次去全局隊列拿 G 的時候,都需要上鎖,避免同樣的任務被多次拿。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果全局隊列都被拿完了,而當前 M 也沒有更多的 G 可以執行的時候,它就會去其他 P 的本地隊列中拿任務,這個機制被稱之爲 work stealing 機制,每次會拿走一半的任務,向下取整,比如另一個 P 中有 3 個任務,那一半就是一個任務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣還有一個特別的場景需要說明,當一個 M 被阻塞時,M 就會與 P 解綁,讓 P 去找其他空閒的 M 綁定執行後面的 G,如果沒有空閒的 M,就會創建一個新的 M。當 M 阻塞結束之後,就會把 G 放入到全局隊列中,這個機制稱之爲 hand off 機制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"work stealing 和 hand off 機制提高了線程的使用效率,避免的線程重複創建和銷燬。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當全局隊列爲空,M 也沒辦法從其他的 P 中拿任務的時候,就會讓自身進入自選狀態,等待有新的 G 進來。最多隻會有 GOMAXPROCS 個 M 在自旋狀態,過多 M 的自旋會浪費 CPU 資源,多餘的 M 的就會與 P 解綁,進入到休眠狀態。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4. 小結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了讓 goroutine 的運行更有效率,Go 實現了一個用戶態的調度器,這個調度器充分利用現代計算機的多核特性,同時讓多個 goroutine 運行,同時 goroutine 設計的很輕量級,調度和上下文切換的代價都比較小。 而且利用 work stealing 和 hand off 機制,對線程進行復用,避免了線程的重複創建。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}}],"text":"文 / Rayjun","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章