記雜||go的協(xié)程到GMP到CSP到context和鎖
本文類似于個人學習筆記不會面面俱到,需要讀者掌握一定基本知識。
線程是輕量級的進程,而協(xié)程是輕量級的線程。進程上下文切換開銷>線程(1~2微秒)>協(xié)程(0.2微秒),以java為例,每個線程都有程序計數(shù)器,虛擬機棧,本地方法棧(默認大小為1MB,創(chuàng)建時指定運行時不可更改);而go的協(xié)程也有棧默認大小僅為2KB,并且運行時可以進行動態(tài)的擴容,僅從內存大小便可以看出協(xié)程的數(shù)量可以比進程大得多。
GMP模型
G指Goroutine既go的協(xié)程,M指線程(原生線程),P指邏輯處理器??梢猿橄罄斫鉃镚通過P在M上得到運行。同時執(zhí)行的g<=p(默認為cpu數(shù)量)<=m(發(fā)生阻塞時可能新建,一般與p相等)
先講一下g0協(xié)程,g0是一種特殊的協(xié)程,每個M都有一個g0,主要作用是執(zhí)行協(xié)程調度的一系列代碼。g使用協(xié)作式+強占式調度,通常g運行結束后會主動退出調度,如果運行時間過長>10ms便會觸發(fā)強占式調度強制讓g退出調度,與m進行解綁,這時便進入到調度循環(huán)中(類似上下文切換),用戶協(xié)程g切換到g0,g0便負責執(zhí)行3個函數(shù),schedule(處理具體的調度策略,選擇下一個執(zhí)行的g),execute(狀態(tài)轉移,綁定g和m),gogo(與操作系統(tǒng)有關函數(shù)),之后便重新切換到新的g上進行運行。
重點講schedule函數(shù):如何找到下一個g,這便是P的主要作用,首先P中有一個runnext字段指向下一個要執(zhí)行的協(xié)程,新創(chuàng)建的g都會搶先放到這里,若為空,則去P內部長度為256的數(shù)組中尋找g,若仍為空,則去不限大小的全局隊列中獲取,通常會把(全局隊列中g / P數(shù)量,最多不能超過128)的g轉移到p的本地隊列中,若全局隊列仍未空,則去其他P中拿取一半的g放到自己的隊列中。注意每61次調度便會優(yōu)先在全局隊列中尋找g,防止過度饑餓。如果p的本地隊列滿了,則會拿取一半 128個g放到全局隊列中。
并且,如果在M執(zhí)行G時發(fā)生了文件IO導致阻塞等,P便會和M進行解綁,重新選擇可以的M或者新建,然后執(zhí)行P中剩余的g,直到g的阻塞結束,注意go對網絡IO進行了異步處理,并不會導致M阻塞,僅阻塞G,M便不會和P解綁,而是去執(zhí)行P的其他g,這么做的好處是如果線程阻塞會涉及到用戶態(tài)和內核態(tài)的切換過程耗時較長,反觀java,由于沒做這種用戶線程和原生線程的分離式設計,遇到線程相關操作便可能會進行內核態(tài)切換,而java的搶占式調度又讓切換變得更加頻繁。
g除了主動調度,搶占式調度外,在io或channel等阻塞時會進行被動調度,p會去尋找下一個g執(zhí)行。
CSP
CSP的思想及通過通信來共享內存,如果你了解java可以發(fā)現(xiàn)java主要是通過共享內存的方式來通信,如果發(fā)生并發(fā)安全問題則通過鎖等機制來解決。這倆個的區(qū)別可以理解為:go中協(xié)程a擁有資源A,而協(xié)程b想要獲取這個資源便在a和b之間架起通道channel,然后a通過channel將A發(fā)送給b;而在java中,a和b會共同使用內存中的資源A,誰想用便去訪問,如果同時需要則進行加鎖。線程間的通信方式不只有這兩種,java還有wait/notify等方式,而go也可以使用共享內存的方式。
channel的使用為基礎知識,這里不進行介紹,簡單介紹select:select可以幫助我們同時監(jiān)控多個通道,若同時面對多個可執(zhí)行通道,select會隨機選取一個來執(zhí)行。
通道底層為一個hchan結構體,有2個隊列,讀取和寫入的阻塞協(xié)程隊列,正常情況下一定有一個隊列為空,既如果有一個g1阻塞在讀隊列中,那么寫的g2便直接將數(shù)據(jù)傳遞給g1。如果為通道設置了緩沖區(qū),則底層會創(chuàng)建一個設定類型的數(shù)組并實現(xiàn)了一個環(huán)形隊列,如果緩沖區(qū)未滿,寫入的g便會直接把數(shù)據(jù)寫入到緩沖區(qū)中不會進行阻塞。注意,g如果進入阻塞隊列也涉及到m和g解綁,m執(zhí)行p中其他的g的過程。
context
如果你不知道如何退出一個協(xié)程,那么就不要創(chuàng)建這個協(xié)程。
context的主要作用便是可以級聯(lián)終止,當父context退出時,使用子context便都會退出,而子context的退出不會影響到父context。通過context我們可以設置超時時間來更好的管理協(xié)程,同時他也內置了一個kv鍵值對,可以用來存放數(shù)據(jù),但使用時需要謹慎,子context可以通過k得到父的v但是由于是一層一層的向上查找,查詢速度較慢。
鎖
go的協(xié)程可以進行通過共享內存來進行通訊,同java為了保證協(xié)程安全同樣需要鎖的機制。go主要有2種鎖:互斥鎖和讀寫鎖,同時也提供了原子操作的實現(xiàn)方式。
面對指令重排問題,go沒有提供volatile但是提供了一系列原子操作,通過其中提供的cas操作+自旋鎖便可以達到原子操作的目標,還可以實現(xiàn)信號量等。
go的互斥鎖和讀寫鎖都是信號量的方式來判斷是直接獲取鎖還是進行等待,如果搶鎖失敗便會進行一段時間的自旋操作,若仍未獲取便根據(jù)信號量來判斷是否需要陷入休眠。go的讀寫鎖遵循著讀讀不互斥,其余互斥的原則,如果申請寫鎖會等所有讀鎖都釋放后再進行操作,反之亦然。
Lotalot你干了什么?!沒有golang八股文我們如何抗衡雙招,Lotalot淡笑一聲:“很簡單,我自己寫不就是了”說完,他氣息終于不再掩飾,顯露而出,Go八股文小解!