从Golang调度器的作者视角探究其设计之道!
導(dǎo)語?|?Golang核心開發(fā)人員、goroutine調(diào)度的設(shè)計(jì)者Dmitry Vyukov,在2019年的一個(gè)talk里深入淺出地闡述了goroutine調(diào)度的設(shè)計(jì)思想以及一些優(yōu)化的細(xì)節(jié)。本文是筆者結(jié)合自身經(jīng)驗(yàn)和認(rèn)知的一點(diǎn)觀后感,采用從零開始層層遞進(jìn)的方法,總結(jié)剖析了其背后的軟件設(shè)計(jì)思想,希望對讀者更好地理解goroutine調(diào)度GMP模型會(huì)有所幫助。
前言
視頻地址:
https://2019.hydraconf.com/2019/talks/7336ginp0kke7n4yxxjvld/
這個(gè)視頻我以前看過,近幾天刷到便又看了一遍,真是有聽君一席話受益匪淺之感。毫不夸張地說,本視頻在筆者看過的所有資料中,對于GMP為什么要有Processor這點(diǎn),講得最為清楚。視頻中對goroutine調(diào)度模型的講解,真可謂深入淺出!下面筆者將自己的一些觀感整理分享給大家,還沒看過視頻的同學(xué),建議先看完本文再去看,收獲會(huì)更大。
為了表達(dá)方便,本文會(huì)沿用golang里面的GMP縮寫:
G ——?goroutine
M —— 機(jī)器線程
P —— 對處理器的抽象
一、設(shè)計(jì)并發(fā)編程模型
goroutine調(diào)度的設(shè)計(jì)目標(biāo),其實(shí)就是設(shè)計(jì)一種高效的并發(fā)編程模型:
從開發(fā)的角度只需要一個(gè)關(guān)鍵詞(go)就能創(chuàng)建一個(gè)執(zhí)行會(huì)話,很方便使用,即開發(fā)效率是高效的。
從運(yùn)行態(tài)的角度,上述創(chuàng)建的會(huì)話也能高效的被調(diào)度執(zhí)行,即運(yùn)行效率也是高效的。
我們可以近似將goroutine看待為協(xié)程(一些代碼邏輯+一個(gè)棧上下文),如果讀者用C/C++造過協(xié)程框架的輪子,會(huì)很容易理解這點(diǎn)。
注:除了高效之外,還有其他幾個(gè)目標(biāo),如無大小限制的goroutine棧,公平的調(diào)度策略等。
二、從零開始:從多線程說起
想要實(shí)現(xiàn)并發(fā)的執(zhí)行流,最直截了當(dāng)?shù)?#xff0c;自然就是多線程。由此便得出初始思路:每個(gè)goroutine對應(yīng)一個(gè)線程。
從并發(fā)的功能角度來講,該方案固然可以實(shí)現(xiàn)并發(fā),但性能方面卻很不堪,尤其是在并發(fā)很重的時(shí)候,成千上萬個(gè)線程的資源占用、創(chuàng)建銷毀、調(diào)度帶來的開銷會(huì)很巨大。
三、更進(jìn)一步:線程池的方案
既然線程太多不好,那我們可以很輕易地做出一點(diǎn)改善,控制一下線程數(shù)量,如此便得到更進(jìn)一步的方案:線程池,限定只啟動(dòng)N個(gè)線程。
由于該方案下,可能是M個(gè)goroutine,N個(gè)線程,因而顯然需要考慮一個(gè)問題:對于一個(gè)goroutine,它到底該由哪個(gè)線程去執(zhí)行?我們可以簡單地采用一個(gè)全局的Global Run Queue,然后讓所有線程主動(dòng)去獲取goroutine來執(zhí)行,示意如下:
這樣做在線程少的時(shí)候,如果調(diào)度行為不是很頻繁,可能問題不大。但當(dāng)線程較多時(shí),就會(huì)有scalable的問題,mutex的互斥競爭會(huì)非常激烈(考慮到基于時(shí)間片的搶占行為,實(shí)際上調(diào)度必然是很頻繁的)。
四、初具雛形:線程分治
在多線程編程領(lǐng)域中,互斥處理可以稱得上是“名聲在外”,需極其小心地去應(yīng)對。最常見的解決方案,并不是如何精妙地去lock free,而是直接通過 “數(shù)據(jù)分治”和“邏輯分治”來避免做復(fù)雜的加鎖互斥,將各個(gè)線程按橫向(載荷分組)或縱向(邏輯劃分)進(jìn)行切分來處理工作。
通過數(shù)據(jù)分治的思想,我們就可以得到改進(jìn)的方案:每個(gè)線程分別處理一批G,進(jìn)行線程分治。將所有G分開放到各線程自己的存儲(chǔ)中,即所謂的Local Run Queue中。示意如下:
注:Global Run Queue也還繼續(xù)存在的,有關(guān)它存在的細(xì)節(jié)非本文重點(diǎn),這里不做展開。
至此,調(diào)度模型已具雛形。
讓我們繼續(xù)分析確認(rèn)一下,該模型是否真的解決了scalable的問題。上述模型下,為了充分利用CPU,每個(gè)線程要按一定的策略去Steal其他線程Local Run Queue里面的G來執(zhí)行,以免線程之間存在load balance問題(有些太閑,有些又太忙)
因此在線程很多的時(shí)候,存在大量的無意義加鎖Steal操作,因?yàn)槠渌€程的Local Run Queue可能也常常都是空的。還有另一個(gè)問題,由于現(xiàn)在的一些內(nèi)存資源是綁定在線程上面的,會(huì)導(dǎo)致線程數(shù)量和資源占用規(guī)模緊耦合。當(dāng)線程數(shù)量多的時(shí)候,資源消耗也會(huì)比較大。
注:在N核的機(jī)器環(huán)境下,假如我們設(shè)定線程池大小為N,由于系統(tǒng)調(diào)用的存在(關(guān)于系統(tǒng)調(diào)用的處理見后文),實(shí)際的線程數(shù)量會(huì)超過N。
五、趨于完善:將資源和線程解耦
既然每個(gè)線程一份資源也不合適,那么我們可以仿照線程池的思路,單獨(dú)做一個(gè)資源池,做計(jì)算存儲(chǔ)分離:把Local Run Queue及相關(guān)存儲(chǔ)資源都挪出去,并依然限定全局一共N份,即可實(shí)現(xiàn)資源規(guī)模與系統(tǒng)中的真實(shí)線程數(shù)量的解耦。線程每次從對應(yīng)的數(shù)據(jù)結(jié)構(gòu)(Processor)中獲取goroutine去執(zhí)行,Local Run Queue及其他一些相關(guān)存儲(chǔ)資源都掛在Processor下。這樣加一層Processor的抽象之后,便得到眾所周知的GMP模型:
現(xiàn)在的調(diào)度模型已趨于完善,不過前面我們主要側(cè)重講的是如何高效,還未討論到調(diào)度的另一個(gè)關(guān)鍵問題:公平性與搶占,接下來我們看看如何實(shí)現(xiàn)搶占。
六、還要公平:調(diào)度搶占
參考操作系統(tǒng)CPU的調(diào)度策略,通常各進(jìn)程會(huì)分時(shí)間片,時(shí)間片用完了就輪到其他進(jìn)程。在golang里也可以如此,不能讓一些goroutine長期霸占著運(yùn)行資源不退出,必須實(shí)現(xiàn)基于時(shí)間片的“搶占”。
那怎么搶占呢,需要監(jiān)測goroutine執(zhí)行時(shí)間片是否用完了。如果要檢查系統(tǒng)中的各種狀態(tài)變化、事件發(fā)生情況,通常會(huì)有中斷與輪詢兩種思路,中斷是由一個(gè)中控方來做檢查與控制,而輪詢則是各個(gè)參與方按一定的策略主動(dòng)check詢問。因此對于goroutine搶占而言,有以下兩種解決方案:
Signals,通過信號(hào)來中斷原來的線程執(zhí)行。
Cooperative checks,通過線程間歇性輪詢自己check運(yùn)行的時(shí)間片情況來主動(dòng)暫停。
二者的優(yōu)劣對比如下:
因?yàn)間olang其實(shí)是有runtime的,而且代碼編譯生成也都是golang編譯器控制的,綜合優(yōu)劣分析,選擇后者會(huì)比較合理。
對于Cooperative checks的方案,從代碼編譯生成的角度看,很容易做check指令的埋點(diǎn)。且因?yàn)間olang本來就要做動(dòng)態(tài)增長棧,在函數(shù)入口處會(huì)插入檢查是否該擴(kuò)棧的指令,正好利用這一點(diǎn)來做相關(guān)的檢查實(shí)現(xiàn)(這里有一些優(yōu)化細(xì)節(jié),可以使得基于時(shí)間片的搶占開銷也較小)
插入check指令的做法,會(huì)導(dǎo)致該方案存在一個(gè)理論缺陷:若有一個(gè)死循環(huán),里面的所有代碼都不包含check指令,那依然會(huì)無法搶占,不過現(xiàn)實(shí)中基本不存在這種情況,總會(huì)做函數(shù)調(diào)用、訪問channel等類似操作,因此不足為慮。
除此以外還有一個(gè)系統(tǒng)調(diào)用的問題,當(dāng)線程一旦進(jìn)入系統(tǒng)調(diào)用后,也會(huì)脫離runtime的控制。試想萬一系統(tǒng)調(diào)用阻塞了呢,基于Cooperative checks的方案,此時(shí)又無法進(jìn)行搶占,是不是整個(gè)線程也就罷工了。所以為了維持整個(gè)調(diào)度體系的高效運(yùn)轉(zhuǎn),必然要在進(jìn)入系統(tǒng)調(diào)用之前要做點(diǎn)什么以防患未然。Dmitry這里采用的辦法也很直接,對于即將進(jìn)入系統(tǒng)調(diào)用的線程,不做搶占,而是由它主動(dòng)讓出執(zhí)行權(quán)。線程A在系統(tǒng)調(diào)用之前handoff讓出Processor的執(zhí)行權(quán),喚醒一個(gè)idle線程B來做交接。當(dāng)線程A從系統(tǒng)調(diào)用返回時(shí),不會(huì)繼續(xù)執(zhí)行,而是將G放到run queue,然后進(jìn)入idle狀態(tài)等待喚醒,這樣一來便能確保活躍線程數(shù)依然與Processor數(shù)量相同。
七、設(shè)計(jì)思想的小結(jié)
這里recap一下,把前文涉及到的一些軟件設(shè)計(jì)思想羅列如下:
線程池,通過多線程提供更大的并發(fā)處理能力,同時(shí)又避免線程過多帶來的過大開銷。
資源池,對有一定規(guī)模約束的資源進(jìn)行池化管理,如內(nèi)存池、機(jī)器池、協(xié)程池等,前面的線程池也可以算作此類。
計(jì)算存儲(chǔ)分離,分別從邏輯、數(shù)據(jù)結(jié)構(gòu)兩個(gè)角度進(jìn)行設(shè)計(jì),規(guī)劃二者的耦合關(guān)系。
加一層,這個(gè)是萬能大法,不贅述。
中斷與輪詢,用于監(jiān)測系統(tǒng)中的各種狀態(tài)變化、事件變化,通常來講中斷會(huì)比輪詢更高效。
八、其他內(nèi)容
本文的重點(diǎn)在GMP模型,因此還有一些其他的內(nèi)容,文中并未詳細(xì)展開:
Local Run Queue里面的G所創(chuàng)建的G會(huì)放到同樣的Local Run Queue(如果滿了還是會(huì)放GRQ),而且會(huì)限制被偷走,這樣可以加強(qiáng)Locality,同時(shí)為了保證公平也做了時(shí)間片繼承,以免不停創(chuàng)建G會(huì)長期霸占運(yùn)行資源。
被搶占的G會(huì)放到全局的G隊(duì)列(Global Run Queue),GRQ會(huì)每61次tick檢查一次,Dmitry針對這個(gè)61解釋了一番,但筆者認(rèn)為還是有點(diǎn)拍腦袋的感覺。
G的棧采用的是Growable stack方案,在函數(shù)入口會(huì)有棧檢查的指令,如需擴(kuò)容棧,會(huì)拷貝到新申請的更大的棧。
Go runtime還會(huì)用Background thread來運(yùn)行一些相對特別的G(如 Network Poller、Timer)。
以上這些內(nèi)容,大家可以去視頻學(xué)習(xí)。
注:本文基于2019的talk,不知最新版本的調(diào)度機(jī)制是否有進(jìn)一步的調(diào)整,不過無論調(diào)整與否,這并不妨礙我們對GMP設(shè)計(jì)思想的學(xué)習(xí)。
九、進(jìn)一步的改進(jìn)
有同學(xué)在與筆者討論時(shí)提了一個(gè)問題:還可以怎么繼續(xù)優(yōu)化,這真的是一個(gè)非常好的問題,這里將該問題的回答也放入文章。
不單純針對GMP,話題稍微放大一點(diǎn),下面簡單聊聊goroutine調(diào)度機(jī)制的一些優(yōu)化可能。
Dmitry自己在視頻最后說的future work方向:
在很多cpu core的情況下,活躍線程數(shù)比較多,work steal的開銷依舊有些浪費(fèi)。
死循環(huán)不含cooperative check指令的這種edge情況的還沒解決。
對于網(wǎng)絡(luò)和timer的goroutine處理是使用全局方式的,不好scale。
以下純屬個(gè)人探討:
首先整體上現(xiàn)在的模型已經(jīng)比較完善,如何進(jìn)一步優(yōu)化要看實(shí)踐場景遇到的問題,以及profile數(shù)據(jù)情況,只有問題和數(shù)據(jù)明確了,才清楚進(jìn)一步工作的宏觀重點(diǎn)(工作中也是,做性能優(yōu)化需要有宏觀視角)。
因?yàn)間oroutine調(diào)度是屬于協(xié)程類的調(diào)度,這里或許可以借鑒原來各種協(xié)程框架的思路做一些對比考慮。
由于筆者并沒細(xì)看過代碼,不大清楚work steal的overhead構(gòu)成,或許可以設(shè)計(jì)其他的rebalance方式,例如換個(gè)視角,不是去steal,而是由runtime統(tǒng)一rebalance再收集派發(fā)。
目前就先想到這些,歡迎討論。
十、歡樂游戲的協(xié)程框架
基于上面那個(gè)問題的回答,這里也補(bǔ)充介紹一下歡樂游戲協(xié)程框架(基于C++)中采用的處理機(jī)制,因?yàn)槭羌儤I(yè)務(wù)自用,所以從設(shè)計(jì)要求上就低很多,不少點(diǎn)直接都可以不去考慮(這也說明了,有些時(shí)候再好的既有流行方案,從性能上講可能也比不過自家的破輪子,當(dāng)然自家的輪子泛化不足,肯定普適性就會(huì)差很多)
協(xié)程調(diào)度采用最簡單的單線程模型
設(shè)計(jì)之初就沒考慮用多線程充分利用多核資源,我們認(rèn)為直接多部署一些進(jìn)程就好。
對于一定要把單進(jìn)程承載做的很高的極少數(shù)場景,可以專事專辦,做專門的方案即可。
協(xié)程采用固定的棧大小
通常幾百k就夠了(例如256k或者512k),創(chuàng)建協(xié)程的時(shí)候就預(yù)分配好。
這點(diǎn)確實(shí)不如growable stack那么高明,但是從實(shí)踐看也算夠了,這樣就免去了stack動(dòng)態(tài)增長的工作(從應(yīng)用編程的視角看,其實(shí)C++里我們可能因?yàn)闊o法做指令插入埋點(diǎn),本來就做不到stack動(dòng)態(tài)增長)。
我們在相鄰stack之間加一些寫保護(hù)page,這樣一旦踩了就會(huì) coredump。
同時(shí)通過編譯選項(xiàng),限制單層棧大小不能超過某個(gè)閾值。
協(xié)程調(diào)度完全不考慮公平性,全部采用主動(dòng)handoff策略
對于某個(gè)協(xié)程,如果它要持續(xù)運(yùn)行,就任它運(yùn)行,直到要進(jìn)行阻塞類操作(典型如RPC調(diào)用),才會(huì)交出執(zhí)行權(quán)。實(shí)際上對于業(yè)務(wù)來講,微觀層面幾十毫秒內(nèi)哪個(gè)協(xié)程多占了一點(diǎn)執(zhí)行權(quán)真的無所謂,不用太講究公平性。假如真的有些協(xié)程餓死了,那說明業(yè)務(wù)都已經(jīng)過載了(就是時(shí)時(shí)刻刻都在跑其他協(xié)程,cpu100),此時(shí)討論公平也沒什么意義了。假如我們真的要做,因?yàn)樽霾坏街噶畈迦?#xff0c;只能采用Signals信號(hào)中斷的方式,在注冊的信號(hào)處理函數(shù)中直接按需切棧。
主協(xié)程主控循環(huán)tick直接管理協(xié)程,協(xié)程調(diào)度不涉及background thread
網(wǎng)絡(luò)IO、第三方異步API tick驅(qū)動(dòng)、timer管理、協(xié)程創(chuàng)建銷毀管理等都是主協(xié)程在做。
主控循環(huán)中,如果要?jiǎng)?chuàng)建或恢復(fù)協(xié)程,就任由它去立即執(zhí)行,一直跑到它阻塞掛起再返回主協(xié)程。
協(xié)程切換示意圖,圖注:1、2、5在主協(xié)程,3、4在業(yè)務(wù)協(xié)程,主協(xié)程和業(yè)務(wù)協(xié)程都在主線程內(nèi)。
仍可以有基于邏輯分治的多線程
框架不是真的只有一個(gè)線程,按功能拆分的日志線程,依然可以存在。
對于一些第三方異步API,如果其tick本身實(shí)現(xiàn)不好,導(dǎo)致大量占據(jù)了運(yùn)行時(shí)間,也可以分拆線程,然后用隊(duì)列之類的機(jī)制和主線程的主協(xié)程交互即可。
對于網(wǎng)絡(luò)IO也同上。
總之,這種基于邏輯分治做線程拆分的改造都是很簡單的,也并不會(huì)影響到核心協(xié)程調(diào)度的機(jī)制。
如果有什么疑問,想深入學(xué)習(xí),歡迎大家加入極客星球,讓我們一起進(jìn)步,掌握核心技術(shù),既能掙錢又能抗壓,掙錢和事業(yè)兩不誤,對星球感興趣的,點(diǎn)擊查看->?極客星球,公眾號(hào)回復(fù)“優(yōu)惠卷”,或者掃描下面二維碼可以加入。里面還有之前幾期的直播分享視頻,星球分享的東西都很干貨。
同時(shí)我每周都會(huì)提問幾道非常經(jīng)典的面試題,通過參與這些經(jīng)典的面試題分析驗(yàn)證,我們可以徹底理解大廠面試的核心知識(shí)點(diǎn),需要深入交流學(xué)習(xí)同學(xué),可以加入極客星球,和大家一起快速成長:
大廠求職核心原理1v1指導(dǎo)(職位,簡歷,面試,策略等一條龍優(yōu)化)
技術(shù)問題幫忙分析解答(有專屬VIP群)
可以加我微信詳細(xì)了解。
大廠技術(shù)路線
后臺(tái)開發(fā)進(jìn)階
開源項(xiàng)目學(xué)習(xí)
直播交流分享(已經(jīng)分享了8期,加入星球可以看回放)
技術(shù)視野
按需提供經(jīng)典資料,節(jié)約你時(shí)間
實(shí)戰(zhàn)技能分享
- END -
看完一鍵三連在看,轉(zhuǎn)發(fā),點(diǎn)贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
定個(gè)目標(biāo)|建立自己的技術(shù)知識(shí)體系
大廠后臺(tái)開發(fā)基本功修煉路線和經(jīng)典資料
個(gè)人學(xué)習(xí)方法分享
你好,這里是極客重生,我是阿榮,大家都叫我榮哥,從華為->外企->到互聯(lián)網(wǎng)大廠,目前是大廠資深工程師,多次獲得五星員工,多年職場經(jīng)驗(yàn),技術(shù)扎實(shí),專業(yè)后端開發(fā)和后臺(tái)架構(gòu)設(shè)計(jì),熱愛底層技術(shù),豐富的實(shí)戰(zhàn)經(jīng)驗(yàn),分享技術(shù)的本質(zhì)原理,希望幫助更多人蛻變重生,拿BAT大廠offer,培養(yǎng)高級工程師能力,成為技術(shù)專家,實(shí)現(xiàn)高薪夢想,期待你的關(guān)注!點(diǎn)擊藍(lán)字查看我的成長之路。
校招/社招/簡歷/面試技巧/大廠技術(shù)棧分析/后端開發(fā)進(jìn)階/優(yōu)秀開源項(xiàng)目/直播分享/技術(shù)視野/實(shí)戰(zhàn)高手等,?極客星球希望成為最有技術(shù)價(jià)值星球,盡最大努力為星球的同學(xué)提供面試,跳槽,技術(shù)成長幫助!詳情查看->極客星球
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點(diǎn)贊,在看,分享三連
總結(jié)
以上是生活随笔為你收集整理的从Golang调度器的作者视角探究其设计之道!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 80%的Linux都不懂的内存问题
- 下一篇: 40张最全计算机网络基础思维导图