前言:
下面是自己看了很多資料后總結下來的不一定對.因為能力還不夠,還不能夠深入源碼一探究竟.
如果自己哪里不對,希望可以留言.一起討論一下.
進程線程協程的理解
1. 進程 :是計算機進行資源分配的最小單位,上下文開銷很大,進程之間數據是隔離的.
2. 線程 :是計算機調度的最小單位,是計算機運行的基本單元.(個人理解 可以把線程理解成是共享堆的進程),上
下文調度相對于進程來說要輕量(因為線程之間調度不需要切換進程資源)
3. 協程 :是用戶態的一種輕量的線程,協程的調度完全由用戶控制. 不需要內核參與,所以速度很快.
go協程模型和特點
協作顧名思義是互相協作的,但是go的協程不完全是互相協作的,還會相互搶占.go底層是基于gmp模型實現的協程調度.
M: 系統真正的線程.m的任務就是想辦法去找到可以運行的g.并且運行它
P: G的集合,有個g的鏈表.一般會和m綁定.m不斷運行p上面的g.并且p會存儲m上線程的相關信息
G: go的協程真正的實體.代表正在要執行的協程的邏輯和其要運行的上下文.
自己對gmp模型的理解(不一定準確)
大家有沒有覺得m->p->g的關系其實就是 計算機->進程->線程的關系. 因為計算機涉及到很多因素的影響,所以對進程
線程之間的調度開銷會很大所以go的作者很有創造性的把這個過程在用戶態自己模擬一套.正因為如此一些簡單的調度都
不需要用到系統來參與,go表示我自己就可以完成了,所以性能要好的很多!
具體GMP是怎么運行的
首先協程實現的功能和線程是一樣的,就是讓我們能夠同時運行多個邏輯的能力(這里指的是并發不是并行).
1. 啟動協程: go程序內部都維護了一個 gfree的一個list.每次我們啟動一個新的協程的時候 程序不會
馬上就new一個g對象.而是先去gfree里找,如果gfree里面有現成的g對象,就會直接拿來用.如果gfree里面
沒有才會去new一個對象,所有的g運行完了都不會直接銷毀.而是放到了gfree里.相當于gfree是一個g的對象池.
總而言之go底層g是復用的.
2. 協程綁定p: 當協程被啟動后就會優先依附于啟動自己的m所綁定的p上,這樣很大程度上避免了m與m之間的競爭.
當m上的p的g已經超過了承受范圍就會放到全局的g的隊列里.與m綁定的p最多只有256個g.
3. m的運行: m就是不斷地運行依附于自己上的p上的g.為了讓全局的g隊列里也能夠被調度到.m每運行61個本地g就會
去全局g隊列里拿一個g運行.當m本地的p沒有g可以運行的時候也會去全局隊列里面拿g運行.當全局隊列也沒有g的時候.
就會去別的m綁定的p上偷一半g來運行.如果這個時候m還是拿不到g.那么這個m就會陷入睡眠.值得注意的是睡眠m也是
不會被銷毀的.防止m被頻繁的創建銷毀.
go的協程 什么時候調度的,怎么調度的
首先我們需要知道go的協作在什么情況會被調度(就是別的協程搶占)
1. 當協程運行時間過長的時候,那么這個協程在調用非內聯函數的時候會被調度:
正式因為有這個特性,go的協程才具有了搶占性,才讓goroutine不再是單純的協程. 首先程序會在啟動的時候運行一個
sysmon線程,這個線程負責整個的調度具有上帝視角.sysmon會給所有的p都記錄一個已運行的g的數量.每當p運行一個g后
都會加1.如果這個變量沒有加1,就說明這個p一直在運行同一個g任務.如果超過了10ms,就給這個g的棧信息上加個標記.
當有了這個標記,這個g在遇到非內聯函數的時候就會把自己中斷掉,移到當前p的隊列尾.
2. 當協程陷入一個系統調用.沒有返回的時候會被調度
當m進行系統調用的時候,這個m會被陷入到內核態,毫無疑問的被阻塞了.還是sysmon會定期檢測,當一個p上的g執行時間過長
且這個g還是在進行系統調用.那么sysmon會把p與當前m解綁, 然后喚醒一個之前沒事做的睡眠的m進行綁定運行.如果沒有
睡眠的m就會重新創建一個m來綁定,之前的m運行著進行系統調用的g返回后就會嘗試找沒有m的p.如果沒有找到,m就會把g放到
全局隊列,然后自己進入睡眠.
3. 當協程被channel阻塞時會被調度
這個不需要多說.當g被channel阻塞后,這個g會被放到相應的wait隊列.該g的狀態由_Gruning變成_Gwating,g會被別的g的
channel操作喚醒,那么這個g就會嘗試加入那個g所在的p的runnext,如果沒有成功,就會加入本地p隊列,全局隊列.等待被m運行.
4. 當協程進行網絡io阻塞的時候會被調度
網絡io雖然也算是系統調用,但是go原生的網絡包底層全是由epoll類似的驅動的,并不走之前系統調用那一套.sysmon線程會進行
epoll_wait,然后對程序每個socket進行標記,哪些socket是可寫的,哪些socket是可讀的,然后嘗試喚醒因為socket不可寫或者
不可讀的g,而g在對socket進行讀寫的時候就判斷sysmon有沒有對其標記過,如果沒有標記過就進入wait狀態,如果標記過了就說明可
用就繼續操作,操作完了,再把這個標記去除了,等待下次標記.
總結:
1. GMP 三者能夠隨意綁定解綁,達到了靈活調度的目的
2. go后臺運行這sysmon線程來進行調度.和上帝一樣.
3. go程序底層有g的對象池,已經對g進行了復用不會被銷毀. 同時m也是不會被銷毀的.所以從某種意義來講go確實越運行越占內存.
參考資料
1. https://juejin.im/post/5b7678f451882533110e8948
2. https://wudaijun.com/2018/01/go-scheduler/
3. https://www.w3cschool.cn/go_internals/go_internals-8h5b282y.html