弄懂goroutine调度原理
goroutine簡介
golang語言作者Rob Pike說,“Goroutine是一個與其他goroutines 并發(fā)運行在同一地址空間的Go函數(shù)或方法。一個運行的程序由一個或更多個goroutine組成。它與線程、協(xié)程、進(jìn)程等不同。它是一個goroutine“。
- goroutine通過通道來通信,而協(xié)程通過讓出和恢復(fù)操作來通信;
- goroutine 通過Golang 的調(diào)度器進(jìn)行調(diào)度,而協(xié)程通過程序本身調(diào)度;
簡單的說就是Golang自己實現(xiàn)了協(xié)程并叫做goruntine(本文稱Go協(xié)程),且比協(xié)程更強大。
goroutine調(diào)度原理
上面說到Go協(xié)程是通過Golang的調(diào)度器進(jìn)行調(diào)度的,其中調(diào)度器的線程模型為兩級線程模型。
有關(guān)兩級線程模型的介紹,可以看這篇文章
我們來看下Golang實現(xiàn)的兩級線程模型是怎樣的。首先要知道這三個字母代表的含義
- M:代表內(nèi)核級的線程
- P:全程Processor,代表運行Go協(xié)程所需要的資源(上下文環(huán)境)
- G:代表Go協(xié)程
我們先看下為實現(xiàn)調(diào)度Golang定義了這些數(shù)據(jù)結(jié)構(gòu)存M,P,G
| 全局M列表 | Go的運行時 | 存放所有M的單向鏈表 |
| 全局P列表 | Go的運行時 | 存放所有P的數(shù)組 |
| 全局G列表 | Go的運行時 | 存放所有G的切片 |
| 調(diào)度器的空閑M列表 | 調(diào)度器 | 存放空閑M的單向鏈表 |
| 調(diào)度器的空閑P列表 | 調(diào)度器 | 存放空閑P的單向鏈表 |
| 調(diào)度器的自由G列表 | 調(diào)度器 | 存放自由G的單向鏈表(有兩個) |
| 調(diào)度器的可運行G隊列 | 調(diào)度器 | 存放可運行G的隊列 |
| P的自由G列表 | 本地P | 存放當(dāng)前P中自由G的單向鏈表 |
| P的可運行G隊列 | 本地P | 存放當(dāng)前P中可運行G的隊列 |
然后從上往下解析Go的兩級線程模型圖
(1)M和內(nèi)核線程之間是一對一的關(guān)系,一個M在其生命周期中,只會和一個內(nèi)核線程關(guān)聯(lián),所以不會出現(xiàn)對內(nèi)核線程的頻繁切換;
Golang的運行時執(zhí)行系統(tǒng)監(jiān)控和垃圾回收等任務(wù)時候會導(dǎo)致創(chuàng)建M,M空閑時不會被銷毀,而是放到一個調(diào)度器的空閑M列表中,等待與P關(guān)聯(lián),M默認(rèn)數(shù)量為10000
(2)P和M之間是多對多的關(guān)系,P和G之間是一對多的關(guān)系,他們的關(guān)聯(lián)是易變的,由Golang的調(diào)度器完成調(diào)度;
Golang的運行時按規(guī)則調(diào)度,讓P和不同的M建立或斷開關(guān)聯(lián),使得P中的G能夠及時獲得運行時機(jī)
(3)P的數(shù)量默認(rèn)為CPU總核心數(shù),最大為256,當(dāng)P沒有可運行的G時候(P的可運行G隊列為空),P會被放到調(diào)度器的空閑P列表中,等待M與它關(guān)聯(lián);
P有可能會被銷毀,如運行時用runtime.GOMAXPROCS把P的數(shù)量從32降到16時,剩余16個會被銷毀,它們原來的G會先轉(zhuǎn)到調(diào)度器可運行的G隊列和自由G列表
(4)每個P中有可運行的G隊列(如圖中最下面的那行G)和自由G列表(圖中未畫出來),當(dāng)G的代碼執(zhí)行完后,該G不會被銷毀,而是被放到P的自由G列表或調(diào)度器的自由G列表。如果程序新建了Go協(xié)程,調(diào)度器會在自由G列表中取一個G,然后把Go協(xié)程的函數(shù)賦值到G中(如果自由G列表為空,就創(chuàng)建一個G);
可見Golang調(diào)度器在調(diào)度時很大程度復(fù)用了M,P,G
(5)在Go程序初始化后,調(diào)度器首先進(jìn)行一輪調(diào)度,此時用M去搜索可運行的G。其中我們的main函數(shù)也是一個G,找到可運行的G后就執(zhí)行它;
至于怎么找可運行的G呢?答案是到處找,想盡辦法找(這里只列出一部分地方)。
- 從本地P的可運行的G隊列找
- 從調(diào)度器的可運行的G隊列找
- 從其他P的可運行的G隊列找
(6)P的可運行G隊列最大只能存放長度為256的G,當(dāng)隊列滿后,調(diào)度器會把一半的G轉(zhuǎn)到調(diào)度器的可運行G隊列。
系統(tǒng)監(jiān)控
上面大概描述了關(guān)于goroutine調(diào)度的流程。現(xiàn)在還存在一個問題,那就是當(dāng)Go協(xié)程很多(并發(fā)量大)時候,顯然G是不能一直執(zhí)行下去的,因為也需要把執(zhí)行機(jī)會留給其他的G。此時Golang運行時的系統(tǒng)監(jiān)控就起作用了。
一般情況,當(dāng)G運行時間超過10ms后,該G就會被系統(tǒng)告知需要停止了,讓其他G運行。(這里情況比較復(fù)雜,并不能確保每個G都能被公平執(zhí)行)
以下特殊情況該G不需要停止
- P的可運行G隊列為空(沒有其他G可運行)
- 有空閑的M在尋找可運行的G(沒有其他G可運行)
- 空閑的P(還有P閑著)
總結(jié)
Golang以兩級線程實現(xiàn)模型,自己實現(xiàn)goruntine和調(diào)度器,優(yōu)勢在于并行和非常低的資源使用。
主要體現(xiàn):
- 內(nèi)存消耗方面(每個Go協(xié)程占的內(nèi)存遠(yuǎn)小于線程占的內(nèi)存)
- 切換(調(diào)度)開銷方面
- 線程切換涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài))
此外,Go協(xié)程執(zhí)行任務(wù)完成的順序并不都是按我們預(yù)期的那樣(程序不加以控制的情況下),特別在一些耗時較長的任務(wù)中。且每個Go協(xié)程執(zhí)行的時間也不是絕對公平的。
如有錯誤地方,還請狂噴!
轉(zhuǎn)載于:https://www.cnblogs.com/FireworksEasyCool/p/11508806.html
總結(jié)
以上是生活随笔為你收集整理的弄懂goroutine调度原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谈谈C#反射(Reflection)
- 下一篇: 对知识图谱的告白:斯坦福大学CS520课