Go實(shí)現(xiàn)一個(gè)協(xié)程池

轉(zhuǎn)載自:超詳細(xì)的講解Go中如何實(shí)現(xiàn)一個(gè)協(xié)程池

并發(fā)(并行)评肆,一直以來(lái)都是一個(gè)編程語(yǔ)言里的核心主題之一,也是被開發(fā)者關(guān)注最多的話題鼠锈;Go語(yǔ)言作為一個(gè)出道以來(lái)就自帶 『高并發(fā)』光環(huán)的富二代編程語(yǔ)言郭膛,它的并發(fā)(并行)編程肯定是值得開發(fā)者去探究的晨抡,而Go語(yǔ)言中的并發(fā)(并行)編程是經(jīng)由goroutine實(shí)現(xiàn)的,goroutine是golang最重要的特性之一则剃,具有使用成本低耘柱、消耗資源低、能效高等特點(diǎn)棍现,官方宣稱原生goroutine并發(fā)成千上萬(wàn)不成問(wèn)題调煎,于是它也成為Gopher們經(jīng)常使用的特性。

Goroutine是優(yōu)秀的己肮,但不是完美的士袄,在極大規(guī)模的高并發(fā)場(chǎng)景下,也可能會(huì)暴露出問(wèn)題谎僻,什么問(wèn)題呢娄柳?又有什么可選的解決方案?本文將通過(guò)runtime對(duì)goroutine的調(diào)度分析艘绍,幫助大家理解它的機(jī)理和發(fā)現(xiàn)一些內(nèi)存和調(diào)度的原理和問(wèn)題赤拒,并且基于此提出一種個(gè)人的解決方案 — 一個(gè)高性能的Goroutine Pool(協(xié)程池)。

Goroutine&Scheduler

Goroutine诱鞠,Go語(yǔ)言基于并發(fā)(并行)編程給出的自家的解決方案挎挖。goroutine是什么?通常goroutine會(huì)被當(dāng)做coroutine(協(xié)程)的 golang實(shí)現(xiàn)般甲,從比較粗淺的層面來(lái)看肋乍,這種認(rèn)知也算是合理,但實(shí)際上敷存,goroutine并非傳統(tǒng)意義上的協(xié)程墓造,現(xiàn)在主流的線程模型分三種:內(nèi)核級(jí)線程模型、用戶級(jí)線程模型和兩級(jí)線程模型(也稱混合型線程模型)锚烦,傳統(tǒng)的協(xié)程庫(kù)屬于用戶級(jí)線程模型觅闽,而goroutine和它的Go Scheduler在底層實(shí)現(xiàn)上其實(shí)是屬于兩級(jí)線程模型,因此涮俄,有時(shí)候?yàn)榱朔奖憷斫饪梢院?jiǎn)單把goroutine類比成協(xié)程蛉拙,但心里一定要有個(gè)清晰的認(rèn)知 — goroutine并不等同于協(xié)程。

線程那些事兒

互聯(lián)網(wǎng)時(shí)代以降彻亲,由于在線用戶數(shù)量的爆炸孕锄,單臺(tái)服務(wù)器處理的連接也水漲船高吮廉,迫使編程模式由從前的串行模式升級(jí)到并發(fā)模型,而幾十年來(lái)畸肆,并發(fā)模型也是一代代地升級(jí)宦芦,有IO多路復(fù)用、多進(jìn)程以及多線程调卑,這幾種模型都各有長(zhǎng)短,現(xiàn)代復(fù)雜的高并發(fā)架構(gòu)大多是幾種模型協(xié)同使用大咱,不同場(chǎng)景應(yīng)用不同模型恬涧,揚(yáng)長(zhǎng)避短,發(fā)揮服務(wù)器的最大性能碴巾,而多線程溯捆,因?yàn)槠漭p量和易用,成為并發(fā)編程中使用頻率最高的并發(fā)模型餐抢,而后衍生的協(xié)程等其他子產(chǎn)品现使,也都基于它低匙,而我們今天要分析的 goroutine 也是基于線程旷痕,因此,我們先來(lái)聊聊線程的三大模型:

線程的實(shí)現(xiàn)模型主要有3種:內(nèi)核級(jí)線程模型顽冶、用戶級(jí)線程模型和兩級(jí)線程模型(也稱混合型線程模型)欺抗,它們之間最大的差異就在于用戶線程與內(nèi)核調(diào)度實(shí)體(KSE,Kernel Scheduling Entity)之間的對(duì)應(yīng)關(guān)系上强重。而所謂的內(nèi)核調(diào)度實(shí)體 KSE 就是指可以被操作系統(tǒng)內(nèi)核調(diào)度器調(diào)度的對(duì)象實(shí)體(這說(shuō)的啥玩意兒绞呈,敢不敢通俗易懂一點(diǎn)?)间景。簡(jiǎn)單來(lái)說(shuō) KSE 就是內(nèi)核級(jí)線程佃声,是操作系統(tǒng)內(nèi)核的最小調(diào)度單元,也就是我們寫代碼的時(shí)候通俗理解上的線程了(這么說(shuō)不就懂了嘛倘要!裝什么13)圾亏。

用戶級(jí)線程模型

用戶線程與內(nèi)核線程KSE是多對(duì)一(N : 1)的映射模型,多個(gè)用戶線程的一般從屬于單個(gè)進(jìn)程并且多線程的調(diào)度是由用戶自己的線程庫(kù)來(lái)完成封拧,線程的創(chuàng)建志鹃、銷毀以及多線程之間的協(xié)調(diào)等操作都是由用戶自己的線程庫(kù)來(lái)負(fù)責(zé)而無(wú)須借助系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)。一個(gè)進(jìn)程中所有創(chuàng)建的線程都只和同一個(gè)KSE在運(yùn)行時(shí)動(dòng)態(tài)綁定泽西,也就是說(shuō)曹铃,操作系統(tǒng)只知道用戶進(jìn)程而對(duì)其中的線程是無(wú)感知的,內(nèi)核的所有調(diào)度都是基于用戶進(jìn)程捧杉。許多語(yǔ)言實(shí)現(xiàn)的 協(xié)程庫(kù) 基本上都屬于這種方式(比如python的gevent)陕见。由于線程調(diào)度是在用戶層面完成的秘血,也就是相較于內(nèi)核調(diào)度不需要讓CPU在用戶態(tài)和內(nèi)核態(tài)之間切換,這種實(shí)現(xiàn)方式相比內(nèi)核級(jí)線程可以做的很輕量級(jí)评甜,對(duì)系統(tǒng)資源的消耗會(huì)小很多直撤,因此可以創(chuàng)建的線程數(shù)量與上下文切換所花費(fèi)的代價(jià)也會(huì)小得多。但該模型有個(gè)原罪:并不能做到真正意義上的并發(fā)蜕着,假設(shè)在某個(gè)用戶進(jìn)程上的某個(gè)用戶線程因?yàn)橐粋€(gè)阻塞調(diào)用(比如I/O阻塞)而被CPU給中斷(搶占式調(diào)度)了谋竖,那么該進(jìn)程內(nèi)的所有線程都被阻塞(因?yàn)閱蝹€(gè)用戶進(jìn)程內(nèi)的線程自調(diào)度是沒(méi)有CPU時(shí)鐘中斷的,從而沒(méi)有輪轉(zhuǎn)調(diào)度)承匣,整個(gè)進(jìn)程被掛起蓖乘。即便是多CPU的機(jī)器,也無(wú)濟(jì)于事韧骗,因?yàn)樵谟脩艏?jí)線程模型下嘉抒,一個(gè)CPU關(guān)聯(lián)運(yùn)行的是整個(gè)用戶進(jìn)程,進(jìn)程內(nèi)的子線程綁定到CPU執(zhí)行是由用戶進(jìn)程調(diào)度的袍暴,內(nèi)部線程對(duì)CPU是不可見的些侍,此時(shí)可以理解為CPU的調(diào)度單位是用戶進(jìn)程。所以很多的協(xié)程庫(kù)會(huì)把自己一些阻塞的操作重新封裝為完全的非阻塞形式政模,然后在以前要阻塞的點(diǎn)上岗宣,主動(dòng)讓出自己,并通過(guò)某種方式通知或喚醒其他待執(zhí)行的用戶線程在該KSE上運(yùn)行淋样,從而避免了內(nèi)核調(diào)度器由于KSE阻塞而做上下文切換耗式,這樣整個(gè)進(jìn)程也不會(huì)被阻塞了。

內(nèi)核級(jí)線程模型

用戶線程與內(nèi)核線程KSE是一對(duì)一(1 : 1)的映射模型趁猴,也就是每一個(gè)用戶線程綁定一個(gè)實(shí)際的內(nèi)核線程刊咳,而線程的調(diào)度則完全交付給操作系統(tǒng)內(nèi)核去做,應(yīng)用程序?qū)€程的創(chuàng)建儡司、終止以及同步都基于內(nèi)核提供的系統(tǒng)調(diào)用來(lái)完成娱挨,大部分編程語(yǔ)言的線程庫(kù)(比如Java的java.lang.Thread、C++11的std::thread等等)都是對(duì)操作系統(tǒng)的線程(內(nèi)核級(jí)線程)的一層封裝捕犬,創(chuàng)建出來(lái)的每個(gè)線程與一個(gè)獨(dú)立的KSE靜態(tài)綁定跷坝,因此其調(diào)度完全由操作系統(tǒng)內(nèi)核調(diào)度器去做。這種模型的優(yōu)勢(shì)和劣勢(shì)同樣明顯:優(yōu)勢(shì)是實(shí)現(xiàn)簡(jiǎn)單或听,直接借助操作系統(tǒng)內(nèi)核的線程以及調(diào)度器探孝,所以CPU可以快速切換調(diào)度線程,于是多個(gè)線程可以同時(shí)運(yùn)行誉裆,因此相較于用戶級(jí)線程模型它真正做到了并行處理顿颅;但它的劣勢(shì)是,由于直接借助了操作系統(tǒng)內(nèi)核來(lái)創(chuàng)建足丢、銷毀和以及多個(gè)線程之間的上下文切換和調(diào)度粱腻,因此資源成本大幅上漲庇配,且對(duì)性能影響很大。

兩級(jí)線程模型

兩級(jí)線程模型是博采眾長(zhǎng)之后的產(chǎn)物绍些,充分吸收前兩種線程模型的優(yōu)點(diǎn)且盡量規(guī)避它們的缺點(diǎn)捞慌。在此模型下,用戶線程與內(nèi)核KSE是多對(duì)多(N : M)的映射模型:首先柬批,區(qū)別于用戶級(jí)線程模型啸澡,兩級(jí)線程模型中的一個(gè)進(jìn)程可以與多個(gè)內(nèi)核線程KSE關(guān)聯(lián),于是進(jìn)程內(nèi)的多個(gè)線程可以綁定不同的KSE氮帐,這點(diǎn)和內(nèi)核級(jí)線程模型相似嗅虏;其次,又區(qū)別于內(nèi)核級(jí)線程模型上沐,它的進(jìn)程里的所有線程并不與KSE一一綁定皮服,而是可以動(dòng)態(tài)綁定同一個(gè)KSE, 當(dāng)某個(gè)KSE因?yàn)槠浣壎ǖ木€程的阻塞操作被內(nèi)核調(diào)度出CPU時(shí)参咙,其關(guān)聯(lián)的進(jìn)程中其余用戶線程可以重新與其他KSE綁定運(yùn)行龄广。所以,兩級(jí)線程模型既不是用戶級(jí)線程模型那種完全靠自己調(diào)度的也不是內(nèi)核級(jí)線程模型完全靠操作系統(tǒng)調(diào)度的蕴侧,而是中間態(tài)(自身調(diào)度與系統(tǒng)調(diào)度協(xié)同工作)择同,也就是 — 『薛定諤的模型』(誤),因?yàn)檫@種模型的高度復(fù)雜性戈盈,操作系統(tǒng)內(nèi)核開發(fā)者一般不會(huì)使用奠衔,所以更多時(shí)候是作為第三方庫(kù)的形式出現(xiàn),而Go語(yǔ)言中的runtime調(diào)度器就是采用的這種實(shí)現(xiàn)方案塘娶,實(shí)現(xiàn)了Goroutine與KSE之間的動(dòng)態(tài)關(guān)聯(lián),不過(guò)Go語(yǔ)言的實(shí)現(xiàn)更加高級(jí)和優(yōu)雅痊夭;該模型為何被稱為兩級(jí)刁岸?即用戶調(diào)度器實(shí)現(xiàn)用戶線程到KSE的『調(diào)度』,內(nèi)核調(diào)度器實(shí)現(xiàn)KSE到CPU上的『調(diào)度』她我。

G-P-M 模型概述

每一個(gè)OS線程都有一個(gè)固定大小的內(nèi)存塊(一般會(huì)是2MB)來(lái)做棧虹曙,這個(gè)棧會(huì)用來(lái)存儲(chǔ)當(dāng)前正在被調(diào)用或掛起(指在調(diào)用其它函數(shù)時(shí))的函數(shù)的內(nèi)部變量。這個(gè)固定大小的棧同時(shí)很大又很小番舆。因?yàn)?MB的棧對(duì)于一個(gè)小小的goroutine來(lái)說(shuō)是很大的內(nèi)存浪費(fèi)酝碳,而對(duì)于一些復(fù)雜的任務(wù)(如深度嵌套的遞歸)來(lái)說(shuō)又顯得太小。因此恨狈,Go語(yǔ)言做了它自己的『線程』疏哗。

在Go語(yǔ)言中,每一個(gè)goroutine是一個(gè)獨(dú)立的執(zhí)行單元禾怠,相較于每個(gè)OS線程固定分配2M內(nèi)存的模式返奉,goroutine的棧采取了動(dòng)態(tài)擴(kuò)容方式贝搁, 初始時(shí)僅為2KB,隨著任務(wù)執(zhí)行按需增長(zhǎng)芽偏,最大可達(dá)1GB(64位機(jī)器最大是1G雷逆,32位機(jī)器最大是256M),且完全由golang自己的調(diào)度器 Go Scheduler 來(lái)調(diào)度污尉。此外膀哲,GC還會(huì)周期性地將不再使用的內(nèi)存回收,收縮棻煌耄空間等太。 因此,Go程序可以同時(shí)并發(fā)成千上萬(wàn)個(gè)goroutine是得益于它強(qiáng)勁的調(diào)度器和高效的內(nèi)存模型蛮放。Go的創(chuàng)造者大概對(duì)goroutine的定位就是屠龍刀缩抡,因?yàn)樗麄儾粌H讓goroutine作為golang并發(fā)編程的最核心組件(開發(fā)者的程序都是基于goroutine運(yùn)行的)而且golang中的許多標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)也到處能見到goroutine的身影,比如net/http這個(gè)包包颁,甚至語(yǔ)言本身的組件runtime運(yùn)行時(shí)和GC垃圾回收器都是運(yùn)行在goroutine上的瞻想,作者對(duì)goroutine的厚望可見一斑。

任何用戶線程最終肯定都是要交由OS線程來(lái)執(zhí)行的娩嚼,goroutine(稱為G)也不例外蘑险,但是G并不直接綁定OS線程運(yùn)行,而是由Goroutine Scheduler中的 P - Logical Processor (邏輯處理器)來(lái)作為兩者的『中介』岳悟,P可以看作是一個(gè)抽象的資源或者一個(gè)上下文佃迄,一個(gè)P綁定一個(gè)OS線程,在golang的實(shí)現(xiàn)里把OS線程抽象成一個(gè)數(shù)據(jù)結(jié)構(gòu):M贵少,G實(shí)際上是由M通過(guò)P來(lái)進(jìn)行調(diào)度運(yùn)行的呵俏,但是在G的層面來(lái)看,P提供了G運(yùn)行所需的一切資源和環(huán)境滔灶,因此在G看來(lái)P就是運(yùn)行它的 “CPU”普碎,由 G、P录平、M 這三種由Go抽象出來(lái)的實(shí)現(xiàn)麻车,最終形成了Go調(diào)度器的基本結(jié)構(gòu):

  • G: 表示Goroutine,每個(gè)Goroutine對(duì)應(yīng)一個(gè)G結(jié)構(gòu)體斗这,G存儲(chǔ)Goroutine的運(yùn)行堆棧动猬、狀態(tài)以及任務(wù)函數(shù),可重用表箭。G并非執(zhí)行體赁咙,每個(gè)G需要綁定到P才能被調(diào)度執(zhí)行。
  • P: Processor,表示邏輯處理器序目, 對(duì)G來(lái)說(shuō)臂痕,P相當(dāng)于CPU核,G只有綁定到P(在P的local runq中)才能被調(diào)度猿涨。對(duì)M來(lái)說(shuō)握童,P提供了相關(guān)的執(zhí)行環(huán)境(Context),如內(nèi)存分配狀態(tài)(mcache)叛赚,任務(wù)隊(duì)列(G)等澡绩,P的數(shù)量決定了系統(tǒng)內(nèi)最大可并行的G的數(shù)量(前提:物理CPU核數(shù) >= P的數(shù)量),P的數(shù)量由用戶設(shè)置的GOMAXPROCS決定俺附,但是不論GOMAXPROCS設(shè)置為多大肥卡,P的數(shù)量最大為256。
  • M: Machine事镣,OS線程抽象步鉴,代表著真正執(zhí)行計(jì)算的資源,在綁定有效的P后璃哟,進(jìn)入schedule循環(huán)氛琢;而schedule循環(huán)的機(jī)制大致是從Global隊(duì)列、P的Local隊(duì)列以及wait隊(duì)列中獲取G随闪,切換到G的執(zhí)行棧上并執(zhí)行G的函數(shù)阳似,調(diào)用goexit做清理工作并回到M,如此反復(fù)铐伴。M并不保留G狀態(tài)撮奏,這是G可以跨M調(diào)度的基礎(chǔ),M的數(shù)量是不定的当宴,由Go Runtime調(diào)整畜吊,為了防止創(chuàng)建過(guò)多OS線程導(dǎo)致系統(tǒng)調(diào)度不過(guò)來(lái),目前默認(rèn)最大限制為10000個(gè)即供。

關(guān)于P定拟,我們需要再絮叨幾句,在Go 1.0發(fā)布的時(shí)候逗嫡,它的調(diào)度器其實(shí)G-M模型,也就是沒(méi)有P的株依,調(diào)度過(guò)程全由G和M完成驱证,這個(gè)模型暴露出一些問(wèn)題:

  • 單一全局互斥鎖(Sched.Lock)和集中狀態(tài)存儲(chǔ)的存在導(dǎo)致所有g(shù)oroutine相關(guān)操作,比如:創(chuàng)建恋腕、重新調(diào)度等都要上鎖抹锄;
  • goroutine傳遞問(wèn)題:M經(jīng)常在M之間傳遞『可運(yùn)行』的goroutine,這導(dǎo)致調(diào)度延遲增大以及額外的性能損耗;
  • 每個(gè)M做內(nèi)存緩存伙单,導(dǎo)致內(nèi)存占用過(guò)高获高,數(shù)據(jù)局部性較差;
  • 由于syscall調(diào)用而形成的劇烈的worker thread阻塞和解除阻塞吻育,導(dǎo)致額外的性能損耗念秧。

這些問(wèn)題實(shí)在太扎眼了,導(dǎo)致Go1.0雖然號(hào)稱原生支持并發(fā)布疼,卻在并發(fā)性能上一直飽受詬病摊趾,然后,Go語(yǔ)言委員會(huì)中一個(gè)核心開發(fā)大佬看不下了游两,親自下場(chǎng)重新設(shè)計(jì)和實(shí)現(xiàn)了Go調(diào)度器(在原有的G-M模型中引入了P)并且實(shí)現(xiàn)了一個(gè)叫做 work-stealing 的調(diào)度算法:

  • 每個(gè)P維護(hù)一個(gè)G的本地隊(duì)列砾层;
  • 當(dāng)一個(gè)G被創(chuàng)建出來(lái),或者變?yōu)榭蓤?zhí)行狀態(tài)時(shí)贱案,就把他放到P的可執(zhí)行隊(duì)列中肛炮;
  • 當(dāng)一個(gè)G在M里執(zhí)行結(jié)束后,P會(huì)從隊(duì)列中把該G取出宝踪;如果此時(shí)P的隊(duì)列為空侨糟,即沒(méi)有其他G可以執(zhí)行, M就隨機(jī)選擇另外一個(gè)P肴沫,從其可執(zhí)行的G隊(duì)列中取走一半粟害。

該算法避免了在goroutine調(diào)度時(shí)使用全局鎖。

至此颤芬,Go調(diào)度器的基本模型確立:

G-P-M模型

G-P-M模型

G-P-M 模型調(diào)度

Go調(diào)度器工作時(shí)會(huì)維護(hù)兩種用來(lái)保存G的任務(wù)隊(duì)列:一種是一個(gè)Global任務(wù)隊(duì)列悲幅,一種是每個(gè)P維護(hù)的Local任務(wù)隊(duì)列。

當(dāng)通過(guò)go關(guān)鍵字創(chuàng)建一個(gè)新的goroutine的時(shí)候站蝠,它會(huì)優(yōu)先被放入P的本地隊(duì)列汰具。為了運(yùn)行g(shù)oroutine,M需要持有(綁定)一個(gè)P菱魔,接著M會(huì)啟動(dòng)一個(gè)OS線程留荔,循環(huán)從P的本地隊(duì)列里取出一個(gè)goroutine并執(zhí)行。當(dāng)然還有上文提及的 work-stealing調(diào)度算法:當(dāng)M執(zhí)行完了當(dāng)前P的Local隊(duì)列里的所有G后澜倦,P也不會(huì)就這么在那躺尸啥都不干聚蝶,它會(huì)先嘗試從Global隊(duì)列尋找G來(lái)執(zhí)行,如果Global隊(duì)列為空藻治,它會(huì)隨機(jī)挑選另外一個(gè)P碘勉,從它的隊(duì)列里中拿走一半的G到自己的隊(duì)列中執(zhí)行。

如果一切正常桩卵,調(diào)度器會(huì)以上述的那種方式順暢地運(yùn)行验靡,但這個(gè)世界沒(méi)這么美好倍宾,總有意外發(fā)生,以下分析goroutine在兩種例外情況下的行為胜嗓。

Go runtime會(huì)在下面的goroutine被阻塞的情況下運(yùn)行另外一個(gè)goroutine:

  • blocking syscall (for example opening a file)
  • network input
  • channel operations
  • primitives in the sync package

這四種場(chǎng)景又可歸類為兩種類型:

用戶態(tài)阻塞/喚醒

當(dāng)goroutine因?yàn)閏hannel操作或者network I/O而阻塞時(shí)(實(shí)際上golang已經(jīng)用netpoller實(shí)現(xiàn)了goroutine網(wǎng)絡(luò)I/O阻塞不會(huì)導(dǎo)致M被阻塞高职,僅阻塞G,這里僅僅是舉個(gè)栗子)辞州,對(duì)應(yīng)的G會(huì)被放置到某個(gè)wait隊(duì)列(如channel的waitq)怔锌,該G的狀態(tài)由_Gruning變?yōu)?code>_Gwaitting,而M會(huì)跳過(guò)該G嘗試獲取并執(zhí)行下一個(gè)G孙技,如果此時(shí)沒(méi)有runnable的G供M運(yùn)行产禾,那么M將解綁P,并進(jìn)入sleep狀態(tài)牵啦;當(dāng)阻塞的G被另一端的G2喚醒時(shí)(比如channel的可讀/寫通知)亚情,G被標(biāo)記為runnable,嘗試加入G2所在P的runnext哈雏,然后再是P的Local隊(duì)列和Global隊(duì)列楞件。

系統(tǒng)調(diào)用阻塞

當(dāng)G被阻塞在某個(gè)系統(tǒng)調(diào)用上時(shí),此時(shí)G會(huì)阻塞在_Gsyscall狀態(tài)裳瘪,M也處于 block on syscall 狀態(tài)土浸,此時(shí)的M可被搶占調(diào)度:執(zhí)行該G的M會(huì)與P解綁,而P則嘗試與其它idle的M綁定彭羹,繼續(xù)執(zhí)行其它G黄伊。如果沒(méi)有其它idle的M,但P的Local隊(duì)列中仍然有G需要執(zhí)行派殷,則創(chuàng)建一個(gè)新的M还最;當(dāng)系統(tǒng)調(diào)用完成后,G會(huì)重新嘗試獲取一個(gè)idle的P進(jìn)入它的Local隊(duì)列恢復(fù)執(zhí)行毡惜,如果沒(méi)有idle的P拓轻,G會(huì)被標(biāo)記為runnable加入到Global隊(duì)列扶叉。

以上就是從宏觀的角度對(duì)Goroutine和它的調(diào)度器進(jìn)行的一些概要性的介紹,當(dāng)然,Go的調(diào)度中更復(fù)雜的搶占式調(diào)度、阻塞調(diào)度的更多細(xì)節(jié)盯滚,大家可以自行去找相關(guān)資料深入理解背率,本文只講到Go調(diào)度器的基本調(diào)度過(guò)程饵筑,為后面自己實(shí)現(xiàn)一個(gè)Goroutine Pool提供理論基礎(chǔ)嫂冻,這里便不再繼續(xù)深入上述說(shuō)的那幾個(gè)調(diào)度了服傍,事實(shí)上如果要完全講清楚Go調(diào)度器,一篇文章的篇幅也實(shí)在是捉襟見肘钞支,所以想了解更多細(xì)節(jié)的同學(xué)可以去看看Go調(diào)度器 G-P-M 模型的設(shè)計(jì)者 Dmitry Vyukov 寫的該模型的設(shè)計(jì)文檔《Go Preemptive Scheduler Design》以及直接去看源碼撼嗓,G-P-M模型的定義放在src/runtime/runtime2.go里面礁遣,而調(diào)度過(guò)程則放在了src/runtime/proc.go里浅碾。

大規(guī)模Goroutine的瓶頸

既然Go調(diào)度器已經(jīng)這么牛逼優(yōu)秀了滥朱,我們?yōu)槭裁催€要自己去實(shí)現(xiàn)一個(gè)golang的 Goroutine Pool 呢帅容?事實(shí)上并徘,優(yōu)秀不代表完美遣钳,任何不考慮具體應(yīng)用場(chǎng)景的編程模式都是耍流氓!有基于G-P-M的Go調(diào)度器背書麦乞,go程序的并發(fā)編程中蕴茴,可以任性地起大規(guī)模的goroutine來(lái)執(zhí)行任務(wù)劝评,官方也宣稱用golang寫并發(fā)程序的時(shí)候隨便起個(gè)成千上萬(wàn)的goroutine毫無(wú)壓力。

然而荐开,你起1000個(gè)goroutine沒(méi)有問(wèn)題付翁,10000也沒(méi)有問(wèn)題,10w個(gè)可能也沒(méi)問(wèn)題晃听;那,100w個(gè)呢砰识?1000w個(gè)呢能扒?(這里只是舉個(gè)極端的例子确丢,實(shí)際編程起這么大規(guī)模的goroutine的例子極少)這里就會(huì)出問(wèn)題等龙,什么問(wèn)題呢悠轩?

  1. 首先嘀趟,即便每個(gè)goroutine只分配2KB的內(nèi)存蹦疑,但如果是恐怖如斯的數(shù)量解幼,聚少成多控乾,內(nèi)存暴漲遂蛀,就會(huì)對(duì)GC造成極大的負(fù)擔(dān)真椿,寫過(guò)java的同學(xué)應(yīng)該知道jvm GC那萬(wàn)惡的STW(Stop The World)機(jī)制鹃答,也就是GC的時(shí)候會(huì)掛起用戶程序直到垃圾回收完,雖然Go1.8之后的GC已經(jīng)去掉了STW以及優(yōu)化成了并行GC突硝,性能上有了不小的提升测摔,但是,如果太過(guò)于頻繁地進(jìn)行GC解恰,依然會(huì)有性能瓶頸锋八;
  2. 其次,還記得前面我們說(shuō)的runtime和GC也都是goroutine嗎护盈?是的挟纱,如果goroutine規(guī)模太大,內(nèi)存吃緊腐宋,runtime調(diào)度和垃圾回收同樣會(huì)出問(wèn)題紊服,雖然G-P-M模型足夠優(yōu)秀,韓信點(diǎn)兵脏款,多多益善围苫,但你不能不給士兵發(fā)口糧(內(nèi)存)吧?巧婦難為無(wú)米之炊撤师,沒(méi)有內(nèi)存剂府,Go調(diào)度器就會(huì)阻塞goroutine,結(jié)果就是P的Local隊(duì)列積壓剃盾,又導(dǎo)致內(nèi)存溢出腺占,這就是個(gè)死循環(huán)…淤袜,甚至極有可能程序直接Crash掉,本來(lái)是想享受golang并發(fā)帶來(lái)的快感效益衰伯,結(jié)果卻得不償失铡羡。

一個(gè)http標(biāo)準(zhǔn)庫(kù)引發(fā)的血案

我想,作為golang擁躉的Gopher們一定都使用過(guò)它的net/http標(biāo)準(zhǔn)庫(kù)意鲸,很多人都說(shuō)用golang寫web server完全可以不用借助第三方的web framework烦周,僅用net/http標(biāo)準(zhǔn)庫(kù)就能寫一個(gè)高性能的web server,的確怎顾,我也用過(guò)它寫過(guò)web server读慎,簡(jiǎn)潔高效,性能表現(xiàn)也相當(dāng)不錯(cuò)槐雾,除非有比較特殊的需求否則一般的確不用借助第三方web framework夭委,但是天下沒(méi)有白吃的午餐,net/http為啥這么快募强?要搞清這個(gè)問(wèn)題株灸,從源碼入手是最好的途徑∏嬷担孔子曾經(jīng)曰過(guò):源碼面前慌烧,如同裸奔。所以幅恋,高清無(wú)碼是阻礙程序猿發(fā)展大大滴絆腳石啊杏死,源碼才是我們進(jìn)步階梯品追,切記切記遭京!

接下來(lái)我們就來(lái)先看看net/http內(nèi)部是怎么實(shí)現(xiàn)的斯嚎。

[圖片上傳失敗...(image-e9d657-1574513923481)]

net/http接收請(qǐng)求且開始處理的源碼放在src/net/http/server.go里,先從入口函數(shù)ListenAndServe進(jìn)去:

 1func (srv *Server) ListenAndServe() error {
 2    addr := srv.Addr
 3    if addr == "" {
 4        addr = ":http"
 5    }
 6    ln, err := net.Listen("tcp", addr)
 7    if err != nil {
 8        return err
 9    }
10    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
11}

看到最后那個(gè)srv.Serve調(diào)用了嗎?沒(méi)錯(cuò)法瑟,這個(gè)Serve方法里面就是實(shí)際處理http請(qǐng)求的邏輯,我們?cè)龠M(jìn)入這個(gè)方法內(nèi)部:

 1func (srv *Server) Serve(l net.Listener) error {
 2    defer l.Close()
 3    ...
 4    // 不斷循環(huán)取出TCP連接
 5    for {
 6        // 看我看我Q纫!竿开!
 7        rw, e := l.Accept()
 8        ...
 9        // 再看我再看我!F槔!破镰!
10        go c.serve(ctx)
11    }
12}

首先,這個(gè)方法的參數(shù)(l net.Listener) 源譬,是一個(gè)TCP監(jiān)聽的封裝霸饲,負(fù)責(zé)監(jiān)聽網(wǎng)絡(luò)端口,rw, e := l.Accept()則是一個(gè)阻塞操作泄伪,從網(wǎng)絡(luò)端口取出一個(gè)新的TCP連接進(jìn)行處理染厅,最后go c.serve(ctx)就是最后真正去處理這個(gè)http請(qǐng)求的邏輯了,看到前面的go關(guān)鍵字了嗎津函?沒(méi)錯(cuò)肖粮,這里啟動(dòng)了一個(gè)新的goroutine去執(zhí)行處理邏輯,而且這是在一個(gè)無(wú)限循環(huán)體里面尔苦,所以意味著涩馆,每來(lái)一個(gè)請(qǐng)求它就會(huì)開一個(gè)goroutine去處理,相當(dāng)任性粗暴啊…允坚,不過(guò)有Go調(diào)度器背書凌净,一般來(lái)說(shuō)也沒(méi)啥壓力,然而屋讶,如果,我是說(shuō)如果哈须教,突然一大波請(qǐng)求涌進(jìn)來(lái)了(比方說(shuō)黑客搞了成千上萬(wàn)的肉雞DDOS你皿渗,沒(méi)錯(cuò)!就這么倒霉G嵯佟)乐疆,這時(shí)候,就很成問(wèn)題了贬养,他來(lái)10w個(gè)請(qǐng)求你就要開給他10w個(gè)goroutine挤土,來(lái)100w個(gè)你就要老老實(shí)實(shí)開給他100w個(gè),線程調(diào)度壓力陡升误算,內(nèi)存爆滿仰美,再然后,你就跪了…

釜底抽薪

有問(wèn)題儿礼,就一定有解決的辦法咖杂,那么,有什么方案可以減緩大規(guī)模goroutine對(duì)系統(tǒng)的調(diào)度和內(nèi)存壓力蚊夫?要想解決問(wèn)題诉字,最重要的是找到造成問(wèn)題的根源,這個(gè)問(wèn)題根源是什么?goroutine的數(shù)量過(guò)多導(dǎo)致資源侵占壤圃,那要解決這個(gè)問(wèn)題就要限制運(yùn)行的goroutine數(shù)量陵霉,合理復(fù)用,節(jié)省資源伍绳,具體就是 — goroutine池化踊挠。

超大規(guī)模并發(fā)的場(chǎng)景下,不加限制的大規(guī)模的goroutine可能造成內(nèi)存暴漲墨叛,給機(jī)器帶來(lái)極大的壓力止毕,吞吐量下降和處理速度變慢還是其次,更危險(xiǎn)的是可能使得程序crash漠趁。所以扁凛,goroutine池化是有其現(xiàn)實(shí)意義的。

首先闯传,100w個(gè)任務(wù)谨朝,是不是真的需要100w個(gè)goroutine來(lái)處理?未必甥绿!用1w個(gè)goroutine也一樣可以處理字币,讓一個(gè)goroutine多處理幾個(gè)任務(wù)就是了嘛,池化的核心優(yōu)勢(shì)就在于對(duì)goroutine的復(fù)用共缕。此舉首先極大減輕了runtime調(diào)度goroutine的壓力洗出,其次,便是降低了對(duì)內(nèi)存的消耗图谷。

[圖片上傳失敗...(image-524b50-1574513923481)]

有一個(gè)商場(chǎng)翩活,來(lái)了1000個(gè)顧客買東西,那么該如何安排導(dǎo)購(gòu)員服務(wù)這1000人呢便贵?有兩種方案:

第一菠镇,我雇1000個(gè)導(dǎo)購(gòu)員實(shí)行一對(duì)一服務(wù),這種當(dāng)然是最高效的承璃,但是太浪費(fèi)資源了利耍,雇1000個(gè)人的成本極高且管理困難,這些可以先按下不表盔粹,但是每個(gè)顧客到商場(chǎng)買東西也不是一進(jìn)來(lái)就馬上買隘梨,一般都得逛一逛,選一選玻佩,也就是得花時(shí)間挑出嘹,1000個(gè)導(dǎo)購(gòu)員一對(duì)一盯著,效率極低咬崔;這就引出第二種方案:我只雇10個(gè)導(dǎo)購(gòu)員税稼,就在商場(chǎng)里待命烦秩,有顧客需要咨詢的時(shí)候招呼導(dǎo)購(gòu)員過(guò)去進(jìn)行處理,導(dǎo)購(gòu)員處理完之后就回來(lái)郎仆,等下一個(gè)顧客需要咨詢的時(shí)候再去只祠,如此往返反復(fù)…

第二種方案有沒(méi)有覺(jué)得很眼熟?沒(méi)錯(cuò)扰肌,其基本思路就是模擬一個(gè)I/O多路復(fù)用抛寝,通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符曙旭,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒)盗舰,能夠通知程序進(jìn)行相應(yīng)的讀寫操作。關(guān)于多路復(fù)用桂躏,不在本文的討論范圍之內(nèi)钻趋,便不再贅述,詳細(xì)原理可以參考 I/O多路復(fù)用剂习。

第一種方案就是net/http標(biāo)準(zhǔn)庫(kù)采用的:來(lái)一個(gè)請(qǐng)求開一個(gè)goroutine處理蛮位;第二種方案就是Goroutine Pool(I/O多路復(fù)用)。

實(shí)現(xiàn)一個(gè)Goroutine Pool

因?yàn)樯鲜鲫惲械囊恍┯捎趃oroutine規(guī)模過(guò)大而可能引發(fā)的問(wèn)題鳞绕,需要有方案來(lái)解決這些問(wèn)題失仁,上文已經(jīng)分析過(guò),把goroutine池化是一種行之有效的方案们何,基于此萄焦,可以實(shí)現(xiàn)一個(gè)Goroutine Pool,復(fù)用goroutine冤竹,減輕runtime的調(diào)度壓力以及緩解內(nèi)存壓力楷扬,依托這些優(yōu)化,在大規(guī)模goroutine并發(fā)的場(chǎng)景下可以極大地提高并發(fā)性能贴见。

哎瑪!前面絮絮叨叨了這么多躲株,終于進(jìn)入正題了片部,接下來(lái)就開始講解如何實(shí)現(xiàn)一個(gè)高性能的Goroutine Pool,秒殺原生并發(fā)的goroutine霜定,在執(zhí)行速度和占用內(nèi)存上提高并發(fā)程序的性能档悠。好了,話不多說(shuō)望浩,開始裝逼分析辖所。

設(shè)計(jì)思路

Goroutine Pool 的實(shí)現(xiàn)思路大致如下:

啟動(dòng)服務(wù)之時(shí)先初始化一個(gè) Goroutine Pool 池,這個(gè)Pool維護(hù)了一個(gè)類似棧的FILO隊(duì)列 磨德,里面存放負(fù)責(zé)處理任務(wù)的Worker缘回,然后在client端提交task到Pool中之后吆视,在Pool內(nèi)部,接收task之后的核心操作是:

  1. 檢查當(dāng)前Worker隊(duì)列中是否有空閑的Worker酥宴,如果有啦吧,取出執(zhí)行當(dāng)前的task;
  2. 沒(méi)有空閑Worker拙寡,判斷當(dāng)前在運(yùn)行的Worker是否已超過(guò)該P(yáng)ool的容量授滓,是 — 阻塞等待直至有Worker被放回Pool;否 — 新開一個(gè)Worker(goroutine)處理肆糕;
  3. 每個(gè)Worker執(zhí)行完任務(wù)之后般堆,放回Pool的隊(duì)列中等待。

調(diào)度過(guò)程如下:

img

按照這個(gè)設(shè)計(jì)思路诚啃,我實(shí)現(xiàn)了一個(gè)高性能的Goroutine Pool淮摔,較好地解決了上述的大規(guī)模調(diào)度和資源占用的問(wèn)題,在執(zhí)行速度和內(nèi)存占用方面相較于原生goroutine并發(fā)占有明顯的優(yōu)勢(shì)绍申,尤其是內(nèi)存占用噩咪,因?yàn)閺?fù)用,所以規(guī)避了無(wú)腦啟動(dòng)大規(guī)模goroutine的弊端极阅,可以節(jié)省大量的內(nèi)存胃碾。

完整的項(xiàng)目代碼可以在我的github上獲取:https://github.com/panjf2000/ants筋搏,也歡迎提意見和交流仆百。

實(shí)現(xiàn)細(xì)節(jié)

Goroutine Pool的設(shè)計(jì)原理前面已經(jīng)講過(guò)了,整個(gè)調(diào)度過(guò)程相信大家應(yīng)該可以理解了奔脐,但是有一句老話說(shuō)得好俄周,空談?wù)`國(guó),實(shí)干興邦髓迎,設(shè)計(jì)思路有了峦朗,具體實(shí)現(xiàn)的時(shí)候肯定會(huì)有很多細(xì)節(jié)、難點(diǎn)排龄,接下來(lái)我們通過(guò)分析這個(gè)Goroutine Pool的幾個(gè)核心實(shí)現(xiàn)以及它們的聯(lián)動(dòng)來(lái)引導(dǎo)大家過(guò)一遍Goroutine Pool的原理波势。

首先是Pool struct

 1type sig struct{}
 2
 3type f func() error
 4
 5// Pool accept the tasks from client,it limits the total
 6// of goroutines to a given number by recycling goroutines.
 7type Pool struct {
 8    // capacity of the pool.
 9    capacity int32
10
11    // running is the number of the currently running goroutines.
12    running int32
13
14    // freeSignal is used to notice pool there are available
15    // workers which can be sent to work.
16    freeSignal chan sig
17
18    // workers is a slice that store the available workers.
19    workers []*Worker
20
21    // release is used to notice the pool to closed itself.
22    release chan sig
23
24    // lock for synchronous operation
25    lock sync.Mutex
26
27    once sync.Once
28}

Pool是一個(gè)通用的協(xié)程池,支持不同類型的任務(wù)橄维,亦即每一個(gè)任務(wù)綁定一個(gè)函數(shù)提交到池中尺铣,批量執(zhí)行不同類型任務(wù),是一種廣義的協(xié)程池争舞;本項(xiàng)目中還實(shí)現(xiàn)了另一種協(xié)程池 — 批量執(zhí)行同類任務(wù)的協(xié)程池PoolWithFunc凛忿,每一個(gè)PoolWithFunc只會(huì)綁定一個(gè)任務(wù)函數(shù)pf,這種Pool適用于大批量相同任務(wù)的場(chǎng)景竞川,因?yàn)槊總€(gè)Pool只綁定一個(gè)任務(wù)函數(shù)店溢,因此PoolWithFunc相較于Pool會(huì)更加節(jié)省內(nèi)存叁熔,但通用性就不如前者了,為了讓大家更好地理解協(xié)程池的原理逞怨,這里我們用通用的Pool來(lái)分析者疤。

capacity是該P(yáng)ool的容量,也就是開啟worker數(shù)量的上限叠赦,每一個(gè)worker綁定一個(gè)goroutine驹马;running是當(dāng)前正在執(zhí)行任務(wù)的worker數(shù)量;freeSignal是一個(gè)信號(hào)除秀,因?yàn)镻ool開啟的worker數(shù)量有上限糯累,因此當(dāng)全部worker都在執(zhí)行任務(wù)的時(shí)候,新進(jìn)來(lái)的請(qǐng)求就需要阻塞等待册踩,那當(dāng)執(zhí)行完任務(wù)的worker被放回Pool之時(shí)泳姐,如何通知阻塞的請(qǐng)求綁定一個(gè)空閑的worker運(yùn)行呢?freeSignal就是來(lái)做這個(gè)事情的暂吉;workers是一個(gè)slice胖秒,用來(lái)存放空閑worker,請(qǐng)求進(jìn)入Pool之后會(huì)首先檢查workers中是否有空閑worker慕的,若有則取出綁定任務(wù)執(zhí)行阎肝,否則判斷當(dāng)前運(yùn)行的worker是否已經(jīng)達(dá)到容量上限,是—阻塞等待肮街,否—新開一個(gè)worker執(zhí)行任務(wù)风题;release是當(dāng)關(guān)閉該P(yáng)ool支持通知所有worker退出運(yùn)行以防goroutine泄露;lock是一個(gè)鎖嫉父,用以支持Pool的同步操作沛硅;once用在確保Pool關(guān)閉操作只會(huì)執(zhí)行一次。

提交任務(wù)到Pool

p.Submit(task f)如下:

1// Submit submit a task to pool
2func (p *Pool) Submit(task f) error {
3    if len(p.release) > 0 {
4        return ErrPoolClosed
5    }
6    w := p.getWorker()
7    w.sendTask(task)
8    return nil
9}

第一個(gè)if判斷當(dāng)前Pool是否已被關(guān)閉绕辖,若是則不再接受新任務(wù)摇肌,否則獲取一個(gè)Pool中可用的worker,綁定該task執(zhí)行仪际。

獲取可用worker(核心)

p.getWorker()源碼:

 1// getWorker returns a available worker to run the tasks.
 2func (p *Pool) getWorker() *Worker {
 3    var w *Worker
 4    // 標(biāo)志朦蕴,表示當(dāng)前運(yùn)行的worker數(shù)量是否已達(dá)容量上限
 5    waiting := false
 6
 7    // 涉及從workers隊(duì)列取可用worker,需要加鎖
 8    p.lock.Lock()
 9    workers := p.workers
10    n := len(workers) - 1
11    // 當(dāng)前worker隊(duì)列為空(無(wú)空閑worker)
12    if n < 0 {
13        // 運(yùn)行worker數(shù)目已達(dá)到該P(yáng)ool的容量上限弟头,置等待標(biāo)志
14        if p.running >= p.capacity {
15            waiting = true
16        // 否則,運(yùn)行數(shù)目加1
17        } else {
18            p.running++
19        }
20    // 有空閑worker涉茧,從隊(duì)列尾部取出一個(gè)使用
21    } else {
22        w = workers[n]
23        workers[n] = nil
24        p.workers = workers[:n]
25    }
26    p.lock.Unlock()
27
28    // 阻塞等待直到有空閑worker
29    if waiting {
30        // 隊(duì)列有空閑worker通知信號(hào)
31        <-p.freeSignal
32        for {
33            p.lock.Lock()
34            workers = p.workers
35            l := len(workers) - 1
36            if l < 0 {
37                p.lock.Unlock()
38                continue
39            }
40            w = workers[l]
41            workers[l] = nil
42            p.workers = workers[:l]
43            p.lock.Unlock()
44            break
45        }
46    // 當(dāng)前無(wú)空閑worker但是Pool還沒(méi)有滿赴恨,
47    // 則可以直接新開一個(gè)worker執(zhí)行任務(wù)
48    } else if w == nil {
49        w = &Worker{
50            pool: p,
51            task: make(chan f),
52        }
53        w.run()
54    }
55    return w
56}

上面的源碼中加了較為詳細(xì)的注釋,結(jié)合前面的設(shè)計(jì)思路伴栓,相信大家應(yīng)該能理解獲取可用worker綁定任務(wù)執(zhí)行這個(gè)協(xié)程池的核心操作伦连,這里主要關(guān)注一個(gè)地方:達(dá)到Pool容量限制之后雨饺,額外的任務(wù)請(qǐng)求需要阻塞等待idle worker,這里是為了防止無(wú)節(jié)制地創(chuàng)建goroutine惑淳,事實(shí)上Go調(diào)度器有一個(gè)復(fù)用機(jī)制额港,每次使用go關(guān)鍵字的時(shí)候它會(huì)檢查當(dāng)前結(jié)構(gòu)體M中的P中,是否有可用的結(jié)構(gòu)體G歧焦。如果有移斩,則直接從中取一個(gè),否則绢馍,需要分配一個(gè)新的結(jié)構(gòu)體G向瓷。如果分配了新的G,需要將它掛到runtime的相關(guān)隊(duì)列中舰涌,但是調(diào)度器卻沒(méi)有限制goroutine的數(shù)量猖任,這在瞬時(shí)性goroutine爆發(fā)的場(chǎng)景下就可能來(lái)不及復(fù)用G而依然創(chuàng)建了大量的goroutine,所以ants除了復(fù)用還做了限制goroutine數(shù)量瓷耙。

其他部分可以依照注釋理解朱躺,這里不再贅述。

任務(wù)執(zhí)行

 1// Worker is the actual executor who runs the tasks,
 2// it starts a goroutine that accepts tasks and
 3// performs function calls.
 4type Worker struct {
 5    // pool who owns this worker.
 6    pool *Pool
 7
 8    // task is a job should be done.
 9    task chan f
10}
11
12// run starts a goroutine to repeat the process
13// that performs the function calls.
14func (w *Worker) run() {
15    go func() {
16        for f := range w.task {
17            if f == nil || len(w.pool.release) > 0 {
18                atomic.AddInt32(&w.pool.running, -1)
19                return
20            }
21            f()
22            w.pool.putWorker(w)
23        }
24    }()
25}
26
27// stop this worker.
28func (w *Worker) stop() {
29    w.task <- nil
30}
31
32// sendTask sends a task to this worker.
33func (w *Worker) sendTask(task f) {
34    w.task <- task
35}

Worker回收(goroutine復(fù)用)

1// putWorker puts a worker back into free pool, recycling the goroutines.
2func (p *Pool) putWorker(worker *Worker) {
3    p.lock.Lock()
4    p.workers = append(p.workers, worker)
5    p.lock.Unlock()
6    p.freeSignal <- sig{}
7}

結(jié)合前面的p.Submit(task f)p.getWorker()搁痛,提交任務(wù)到Pool之后长搀,獲取一個(gè)可用worker,每新建一個(gè)worker實(shí)例之時(shí)都需要調(diào)用w.run()啟動(dòng)一個(gè)goroutine監(jiān)聽worker的任務(wù)列表task落追,一有任務(wù)提交進(jìn)來(lái)就執(zhí)行盈滴;所以,當(dāng)調(diào)用worker的sendTask(task f)方法提交任務(wù)到worker的任務(wù)隊(duì)列之后轿钠,馬上就可以被接收并執(zhí)行巢钓,當(dāng)任務(wù)執(zhí)行完之后,會(huì)調(diào)用w.pool.putWorker(w *Worker)方法將這個(gè)已經(jīng)執(zhí)行完任務(wù)的worker從當(dāng)前任務(wù)解綁放回Pool中疗垛,以供下個(gè)任務(wù)可以使用症汹,至此,一個(gè)任務(wù)從提交到完成的過(guò)程就此結(jié)束贷腕,Pool調(diào)度將進(jìn)入下一個(gè)循環(huán)背镇。

概括起來(lái),ants Goroutine Pool的調(diào)度過(guò)程圖示如下:

img
img
img
img

[圖片上傳失敗...(image-70b36d-1574513923482)]

彩蛋

還記得前面我說(shuō)除了通用的Pool struct之外泽裳,本項(xiàng)目還實(shí)現(xiàn)了一個(gè)PoolWithFunc struct—一個(gè)執(zhí)行批量同類任務(wù)的協(xié)程池瞒斩,PoolWithFunc相較于Pool,因?yàn)橐粋€(gè)池只綁定一個(gè)任務(wù)函數(shù)涮总,省去了每一次task都需要傳送一個(gè)任務(wù)函數(shù)的代價(jià)胸囱,因此其性能優(yōu)勢(shì)比起Pool更明顯,這里我們稍微講一下一個(gè)協(xié)程池只綁定一個(gè)任務(wù)函數(shù)的細(xì)節(jié):

上碼瀑梗!

 1type pf func(interface{}) error
 2
 3// PoolWithFunc accept the tasks from client,it limits the total
 4// of goroutines to a given number by recycling goroutines.
 5type PoolWithFunc struct {
 6    // capacity of the pool.
 7    capacity int32
 8
 9    // running is the number of the currently running goroutines.
10    running int32
11
12    // freeSignal is used to notice pool there are available
13    // workers which can be sent to work.
14    freeSignal chan sig
15
16    // workers is a slice that store the available workers.
17    workers []*WorkerWithFunc
18
19    // release is used to notice the pool to closed itself.
20    release chan sig
21
22    // lock for synchronous operation
23    lock sync.Mutex
24
25    // pf is the function for processing tasks
26    poolFunc pf
27
28    once sync.Once
29}

PoolWithFunc struct中的大部分字段和Pool struct基本一致烹笔,重點(diǎn)關(guān)注poolFunc pf裳扯,這是一個(gè)函數(shù)類型,也就是該P(yáng)ool綁定的指定任務(wù)函數(shù)谤职,而client提交到這種類型的Pool的數(shù)據(jù)就不再是一個(gè)任務(wù)函數(shù)task f了饰豺,而是poolFunc pf任務(wù)函數(shù)的形參,然后交由WorkerWithFunc處理:

 1// WorkerWithFunc is the actual executor who runs the tasks,
 2// it starts a goroutine that accepts tasks and
 3// performs function calls.
 4type WorkerWithFunc struct {
 5    // pool who owns this worker.
 6    pool *PoolWithFunc
 7
 8    // args is a job should be done.
 9    args chan interface{}
10}
11
12// run starts a goroutine to repeat the process
13// that performs the function calls.
14func (w *WorkerWithFunc) run() {
15    go func() {
16        for args := range w.args {
17            if args == nil || len(w.pool.release) > 0 {
18                atomic.AddInt32(&w.pool.running, -1)
19                return
20            }
21            w.pool.poolFunc(args)
22            w.pool.putWorker(w)
23        }
24    }()
25}

上面的源碼可以看到WorkerWithFunc是一個(gè)類似Worker的結(jié)構(gòu)允蜈,只不過(guò)監(jiān)聽的是函數(shù)的參數(shù)隊(duì)列冤吨,每接收到一個(gè)參數(shù)包,就直接調(diào)用PoolWithFunc綁定好的任務(wù)函數(shù)poolFunc pf任務(wù)函數(shù)執(zhí)行任務(wù)陷寝,接下來(lái)的流程就和Worker是一致的了锅很,執(zhí)行完任務(wù)后就把worker放回協(xié)程池,等待下次使用凤跑。

至于其他邏輯如提交task爆安、獲取Worker綁定任務(wù)等基本復(fù)用自Pool struct,具體細(xì)節(jié)有細(xì)微差別仔引,但原理一致扔仓,萬(wàn)變不離其宗,有興趣的同學(xué)可以看我在github上的源碼:Goroutine Pool協(xié)程池 ants:https://github.com/panjf2000/ants 咖耘。

Benchmarks

吹了這么久的Goroutine Pool翘簇,那都是虛的,理論上池化可以復(fù)用goroutine儿倒,提升性能節(jié)省內(nèi)存版保,沒(méi)有benchmark數(shù)據(jù)之前,好像也不能服眾哈夫否!所以彻犁,本章就來(lái)進(jìn)行一次實(shí)測(cè),驗(yàn)證一下再大規(guī)模goroutine并發(fā)的場(chǎng)景下凰慈,Goroutine Pool的表現(xiàn)是不是真的比原生Goroutine并發(fā)更好汞幢!

測(cè)試機(jī)器參數(shù):

1OS : macOS High Sierra
2Processor : 2.7 GHz Intel Core i5
3Memory : 8 GB 1867 MHz DDR3

Pool測(cè)試

測(cè)試代碼傳送門:https://github.com/panjf2000/ants/blob/master/ants_test.go

測(cè)試結(jié)果:

[圖片上傳失敗...(image-3cc978-1574513923482)]

這里為了模擬大規(guī)模goroutine的場(chǎng)景,兩次測(cè)試的并發(fā)次數(shù)分別是100w和1000w微谓,前兩個(gè)測(cè)試分別是執(zhí)行100w個(gè)并發(fā)任務(wù)不使用Pool和使用了ants的Goroutine Pool的性能森篷,后兩個(gè)則是1000w個(gè)任務(wù)下的表現(xiàn),可以直觀的看出在執(zhí)行速度和內(nèi)存使用上豺型,ants的Pool都占有明顯的優(yōu)勢(shì)仲智。100w的任務(wù)量,使用ants姻氨,執(zhí)行速度與原生goroutine相當(dāng)甚至略快钓辆,但只實(shí)際使用了不到5w個(gè)goroutine完成了全部任務(wù),且內(nèi)存消耗僅為原生并發(fā)的40%;而當(dāng)任務(wù)量達(dá)到1000w岩馍,優(yōu)勢(shì)則更加明顯了:用了70w左右的goroutine完成全部任務(wù),執(zhí)行速度比原生goroutine提高了100%抖韩,且內(nèi)存消耗依舊保持在不使用Pool的40%左右蛀恩。

PoolWithFunc測(cè)試

測(cè)試代碼傳送門:

https://github.com/panjf2000/ants/blob/master/ants_benchmark_test.go

測(cè)試結(jié)果:

[圖片上傳失敗...(image-9b85bf-1574513923482)]

  • Benchmarkxxx-4 格式為基準(zhǔn)測(cè)試函數(shù)名-GOMAXPROCS,后面的-4代表測(cè)試函數(shù)運(yùn)行時(shí)對(duì)應(yīng)的CPU核數(shù)
  • 1 表示執(zhí)行的次數(shù)
  • xx ns/op 表示每次的執(zhí)行時(shí)間
  • xx B/op 表示每次執(zhí)行分配的總字節(jié)數(shù)(內(nèi)存消耗)
  • xx allocs/op 表示每次執(zhí)行發(fā)生了多少次內(nèi)存分配

因?yàn)?code>PoolWithFunc這個(gè)Pool只綁定一個(gè)任務(wù)函數(shù)茂浮,也即所有任務(wù)都是運(yùn)行同一個(gè)函數(shù)双谆,所以相較于Pool對(duì)原生goroutine在執(zhí)行速度和內(nèi)存消耗的優(yōu)勢(shì)更大,上面的結(jié)果可以看出席揽,執(zhí)行速度可以達(dá)到原生goroutine的300%顽馋,而內(nèi)存消耗的優(yōu)勢(shì)已經(jīng)達(dá)到了兩位數(shù)的差距,原生goroutine的內(nèi)存消耗達(dá)到了ants的35倍且原生goroutine的每次執(zhí)行的內(nèi)存分配次數(shù)也達(dá)到了ants45倍幌羞,1000w的任務(wù)量寸谜,ants的初始分配容量是5w,因此它完成了所有的任務(wù)依舊只使用了5w個(gè)goroutine属桦!事實(shí)上熊痴,ants的Goroutine Pool的容量是可以自定義的,也就是說(shuō)使用者可以根據(jù)不同場(chǎng)景對(duì)這個(gè)參數(shù)進(jìn)行調(diào)優(yōu)直至達(dá)到最高性能聂宾。

吞吐量測(cè)試

上面的benchmarks出來(lái)以后果善,我當(dāng)時(shí)的內(nèi)心是這樣的:

[圖片上傳失敗...(image-c8e050-1574513923482)]

但是太順利反而讓我疑惑,因?yàn)榻Y(jié)合我過(guò)去這20幾年的坎坷人生來(lái)看系谐,事情應(yīng)該不會(huì)這么美好才對(duì)巾陕,果不其然,細(xì)細(xì)一想纪他,雖然ants Groutine Pool能在大規(guī)模并發(fā)下執(zhí)行速度和內(nèi)存消耗都對(duì)原生goroutine占有明顯優(yōu)勢(shì)鄙煤,但前面的測(cè)試demo相信大家注意到了,里面使用了WaitGroup止喷,也就是用來(lái)對(duì)goroutine同步的工具馆类,所以上面的benchmarks中主進(jìn)程會(huì)等待所有子goroutine完成任務(wù)后才算完成一次性能測(cè)試,然而又有多少場(chǎng)景是單臺(tái)機(jī)器需要扛100w甚至1000w同步任務(wù)的弹谁?基本沒(méi)有扒伞!結(jié)果就是造出了屠龍刀预愤,可是世界上沒(méi)有龍肮涤凇!也是無(wú)情…

彼時(shí)植康,我內(nèi)心變成了這樣:

[圖片上傳失敗...(image-699038-1574513923482)]

幸好旷太,ants在同步批量任務(wù)方面有點(diǎn)曲高和寡,但是如果是異步批量任務(wù)的場(chǎng)景下,就有用武之地了供璧,也就是說(shuō)存崖,在大批量的任務(wù)無(wú)須同步等待完成的情況下,可以再測(cè)一下ants和原生goroutine并發(fā)的性能對(duì)比睡毒,這個(gè)時(shí)候的性能對(duì)比也即是吞吐量對(duì)比了来惧,就是在相同大規(guī)模數(shù)量的請(qǐng)求涌進(jìn)來(lái)的時(shí)候,ants和原生goroutine誰(shuí)能用更快的速度演顾、更少的內(nèi)存『吞』完這些請(qǐng)求供搀。

測(cè)試代碼傳送門:

https://github.com/panjf2000/ants/blob/master/ants_benchmark_test.go

測(cè)試結(jié)果:

10w 吞吐量

img

100w 吞吐量

img

1000W 吞吐量

img

因?yàn)樵谖业碾娔X上測(cè)試1000w吞吐量的時(shí)候原生goroutine已經(jīng)到了極限,因此程序直接把電腦拖垮了钠至,無(wú)法正常測(cè)試了葛虐,所以1000w吞吐的測(cè)試數(shù)據(jù)只有antsPool的。

從該demo測(cè)試吞吐性能對(duì)比可以看出棉钧,使用ants的吞吐性能相較于原生goroutine可以保持在26倍的性能壓制屿脐,而內(nèi)存消耗則可以達(dá)到1020倍的節(jié)省優(yōu)勢(shì)。

總結(jié)

至此掰盘,一個(gè)高性能的 Goroutine Pool 開發(fā)就完成了摄悯,事實(shí)上,原理不難理解愧捕,總結(jié)起來(lái)就是一個(gè)『復(fù)用』奢驯,具體落實(shí)到代碼細(xì)節(jié)就是鎖同步、原子操作次绘、channel通信等這些技巧的使用如孝,ant這整個(gè)項(xiàng)目沒(méi)有借助任何第三方的庫(kù)未妹,用golang的標(biāo)準(zhǔn)庫(kù)就完成了所有功能最铁,因?yàn)楸旧韌olang的語(yǔ)言原生庫(kù)已經(jīng)足夠優(yōu)秀棵里,很多時(shí)候開發(fā)golang項(xiàng)目的時(shí)候是可以保持輕量且高性能的,未必事事需要借助第三方庫(kù)禾进。

關(guān)于ants的價(jià)值豁跑,其實(shí)前文也提及過(guò)了,ants在大規(guī)模的異步&同步批量任務(wù)處理都有著明顯的性能優(yōu)勢(shì)(特別是異步批量任務(wù))泻云,而單機(jī)上百萬(wàn)上千萬(wàn)的同步批量任務(wù)處理現(xiàn)實(shí)意義不大艇拍,但是在異步批量任務(wù)處理方面有很大的應(yīng)用價(jià)值,所以我個(gè)人覺(jué)得宠纯,Goroutine Pool真正的價(jià)值還是在:

  1. 限制并發(fā)的goroutine數(shù)量卸夕;
  2. 復(fù)用goroutine,減輕runtime調(diào)度壓力婆瓜,提升程序性能快集;
  3. 規(guī)避過(guò)多的goroutine侵占系統(tǒng)資源(CPU&內(nèi)存)贡羔。

本文項(xiàng)目源碼:https://github.com/panjf2000/ants

后記

Go語(yǔ)言的三位最初的締造者 — Rob Pike、Robert Griesemer 和 Ken Thompson 中个初,Robert Griesemer 參與設(shè)計(jì)了Java的HotSpot虛擬機(jī)和Chrome瀏覽器的JavaScript V8引擎乖寒,Rob Pike 在大名鼎鼎的bell lab侵淫多年,參與了Plan9操作系統(tǒng)院溺、C編譯器以及多種語(yǔ)言編譯器的設(shè)計(jì)和實(shí)現(xiàn)宵统,Ken Thompson 更是圖靈獎(jiǎng)得主、Unix之父覆获、C語(yǔ)言之父。這三人在計(jì)算機(jī)史上可是元老級(jí)別的人物瓢省,特別是 Ken Thompson 弄息,是一手締造了Unix和C語(yǔ)言計(jì)算機(jī)領(lǐng)域的上古大神,所以Go語(yǔ)言的設(shè)計(jì)哲學(xué)有著深深的Unix烙忧诨椤:簡(jiǎn)單摹量、模塊化、正交馒胆、組合缨称、pipe、功能短小且聚焦等祝迂;而令許多開發(fā)者青睞于Go的簡(jiǎn)潔睦尽、高效編程模式的原因,也正在于此型雳。

Go語(yǔ)言的三個(gè)爸爸

Go語(yǔ)言的三個(gè)爸爸

本文從三大線程模型到Go并發(fā)調(diào)度器再到自定制的 Goroutine Pool当凡,算是較為完整的窺探了整個(gè)Go語(yǔ)言并發(fā)模型的前世今生,我們也可以看到纠俭,Go的設(shè)計(jì)當(dāng)然不完美沿量,比如一直被詬病的error處理模式、不支持泛型冤荆、差強(qiáng)人意的包管理以及面向?qū)ο竽J降倪^(guò)度抽象化等等朴则,實(shí)際上沒(méi)有任何一門編程語(yǔ)言敢說(shuō)自己是完美的,還是那句話钓简,任何不考慮應(yīng)用場(chǎng)景和語(yǔ)言定位的爭(zhēng)執(zhí)都毫無(wú)意義乌妒,而Go的定位從出道開始就是系統(tǒng)編程語(yǔ)言&云計(jì)算編程語(yǔ)言(這個(gè)有點(diǎn)模糊),而Go的作者們也一直堅(jiān)持的是用最簡(jiǎn)單抽象的工程化設(shè)計(jì)完成最復(fù)雜的功能涌庭,所以如果從這個(gè)層面去看Go的并發(fā)模型芥被,就可以看出其實(shí)除了G-P-M模型中引入的 P ,并沒(méi)有太多革新的原創(chuàng)理論坐榆,兩級(jí)線程模型是早已成熟的理論拴魄,搶占式調(diào)度更不是什么新鮮的調(diào)度模式,Go的偉大之處是在于它誕生之初就是依照Go在谷歌:以軟件工程為目的的語(yǔ)言設(shè)計(jì)而設(shè)計(jì)的,Go其實(shí)就是將這些經(jīng)典的理論和技術(shù)以一種優(yōu)雅高效的工程化方式組合了起來(lái)匹中,并用簡(jiǎn)單抽象的API或語(yǔ)法糖開放給使用者夏漱,Go一直致力于找尋一個(gè)高性能&開發(fā)效率的雙贏點(diǎn),目前為止顶捷,它做得遠(yuǎn)不夠完美挂绰,但足夠優(yōu)秀。另外Go通過(guò)引入channel與goroutine協(xié)同工作服赎,將一種區(qū)別于鎖&原子操作的并發(fā)編程模式 — CSP 帶入了Go語(yǔ)言葵蒂,對(duì)開發(fā)人員在并發(fā)編程模式上的思考有很大的啟發(fā)。

從本文中對(duì)Go調(diào)度器的分析以及antsGoroutine Pool 的設(shè)計(jì)與實(shí)現(xiàn)過(guò)程重虑,對(duì)Go的并發(fā)模型做了一次解構(gòu)和優(yōu)化思考践付,在ants中的代碼實(shí)現(xiàn)對(duì)鎖同步、原子操作缺厉、channel通信的使用也做了一次較為全面的實(shí)踐永高,希望對(duì)Gopher們?cè)贕o語(yǔ)言并發(fā)模型與并發(fā)編程的理解上能有所裨益。

感謝閱讀提针。

參考

  • Go并發(fā)編程實(shí)戰(zhàn)(第2版)
  • Go語(yǔ)言學(xué)習(xí)筆記
  • go-coding-in-go-way
  • 也談goroutine調(diào)度器
  • [The Go scheduler]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末命爬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辐脖,更是在濱河造成了極大的恐慌饲宛,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗜价,死亡現(xiàn)場(chǎng)離奇詭異落萎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)炭剪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門练链,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人奴拦,你說(shuō)我怎么就攤上這事媒鼓。” “怎么了错妖?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵绿鸣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我暂氯,道長(zhǎng)潮模,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任痴施,我火速辦了婚禮擎厢,結(jié)果婚禮上究流,老公的妹妹穿的比我還像新娘。我一直安慰自己动遭,他們只是感情好芬探,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著厘惦,像睡著了一般偷仿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宵蕉,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天酝静,我揣著相機(jī)與錄音,去河邊找鬼羡玛。 笑死形入,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的缝左。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼浓若,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼渺杉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起挪钓,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤是越,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后碌上,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倚评,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年馏予,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了天梧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡霞丧,死狀恐怖呢岗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛹尝,我是刑警寧澤后豫,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站突那,受9級(jí)特大地震影響挫酿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愕难,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一早龟、第九天 我趴在偏房一處隱蔽的房頂上張望惫霸。 院中可真熱鬧,春花似錦拄衰、人聲如沸它褪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)茫打。三九已至,卻和暖如春妖混,著一層夾襖步出監(jiān)牢的瞬間老赤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工制市, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抬旺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓祥楣,卻偏偏與公主長(zhǎng)得像开财,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子误褪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容