手?jǐn)]一款簡(jiǎn)單高效的線程池(一)
在之前的幾篇文章中,我們簡(jiǎn)單介紹了圖化框架的執(zhí)行邏輯和循環(huán)邏輯,也指出了圖中無(wú)依賴的節(jié)點(diǎn)需要放入線程池中并行執(zhí)行。
線程池大家應(yīng)該都用過(guò),不過(guò)如何從 0 到 1 的設(shè)計(jì)一款簡(jiǎn)單好用且性能較好的線程池?我們?cè)诮酉聛?lái)的幾篇文章中,為您一一介紹。
大家好,我是不會(huì)寫(xiě)代碼的純序員——Chunel Feng(個(gè)人博客鏈接[1])。前段時(shí)間,在日常搬(hua)磚(shui)之余,我花了一些時(shí)間對(duì)CGraph中的底層線程池(C++版本)進(jìn)行了優(yōu)化。引入了一些優(yōu)秀的機(jī)制,并進(jìn)行了相關(guān)性能測(cè)試。
在這里跟大家分享一下其中的一些技巧和成果:主要包含了 thread-local 機(jī)制、任務(wù)盜取機(jī)制、負(fù)載均衡機(jī)制、lock-free 機(jī)制、自動(dòng)擴(kuò)縮容機(jī)制、批量任務(wù)處理機(jī)制等,并且會(huì)在本系列的最后給出一些性能指標(biāo)的對(duì)比。
當(dāng)然,以下這些內(nèi)容也僅局限在我自己的認(rèn)知范圍,如果大家有什么好的意見(jiàn)和建議,歡迎大家隨時(shí)提出。如果能幫忙測(cè)出幾個(gè) bug,那就更好了,哈哈哈哈。

在開(kāi)始介紹之前,我們照例先提供出來(lái)CGraph 源碼鏈接[2]哈。其中,線程池相關(guān)的部分在 /src/UtilsCtrl/ThreadPool/ 文件夾下。
背景介紹
圖框架中,會(huì)涉及到有些彼此不依賴任務(wù),需要并發(fā)執(zhí)行。這個(gè)時(shí)候,如果是在執(zhí)行任務(wù)的過(guò)程中,通過(guò)反復(fù)開(kāi)閉線程來(lái)實(shí)現(xiàn),就有點(diǎn)太說(shuō)不過(guò)去了。我們需要通過(guò)一個(gè)高性能的線程池來(lái)對(duì)線程進(jìn)行統(tǒng)一管控和循環(huán)使用。
相比 Java、Go 等高級(jí)語(yǔ)言,原生的 C++在多線程方面落后的可不是一星半點(diǎn)。就拿 Java 來(lái)說(shuō)吧,在語(yǔ)言層面就提供了好幾種線程池的封裝和實(shí)現(xiàn),各種同步/互斥機(jī)制也很完善。完善到什么程度捏,嗯,就這樣說(shuō)吧,連我都能用,嘿嘿。
但提到 C++,就比較呵呵了。在 C++11 版本之前,別說(shuō)是線程調(diào)度了,就連最簡(jiǎn)單的開(kāi)辟線程功能,都只能依賴第三方庫(kù)實(shí)現(xiàn)——順便說(shuō)一句,boost 庫(kù)作為 std 庫(kù)的“提前體驗(yàn)”版本,其中也包含 threadpool 的功能,也不知道有多少人知道,多少人用過(guò)。
C++11 版本之后,官方也開(kāi)始學(xué)(co)習(xí)(py)Java,提供了一些語(yǔ)言標(biāo)準(zhǔn)庫(kù)層面的支持,比如 std::thread,std::mutex,std::unique_lock,std::promise,std::atomic 等,進(jìn)而還有后面更新一些的版本支持的 std::shared_mutex,std::shared_time_mutex 等功能。不過(guò),直到今天也還是缺失了很多功能,比如官方 threadpool,又比如同步屏障(barrier)等。再多說(shuō)一句,啥時(shí)候能有靠譜一點(diǎn)的協(xié)程啊???在線等,挺急的!??!
在開(kāi)始開(kāi)發(fā)之前,本著學(xué)(zhan)習(xí)(tie)借(fu)鑒(zhi)的優(yōu)秀態(tài)度,我也在 github 和 B 乎上看了一些 C++線程池的實(shí)現(xiàn)。有的版本 star 是挺多的哈,但給人感覺(jué)基本上還是比較 simple 的,單純是為了實(shí)現(xiàn)功能而實(shí)現(xiàn),并沒(méi)有太多性能方面的考量和優(yōu)化。

于是我決定自己從頭開(kāi)始實(shí)現(xiàn)一個(gè)版本。至少咱要比 github 上搜到的要好吧,否則咱都不好意思說(shuō)咱是從 porn*hub 社區(qū)來(lái)的了。反正我認(rèn)識(shí)這么多老師,不懂的就問(wèn)就是了。
提出問(wèn)題
通常,在講(tree)優(yōu)(new)勢(shì)(bee)之前,都要先踩一下其他的競(jìng)品的不足。上學(xué)參加競(jìng)賽的時(shí)候如是,追女神的時(shí)候如是,競(jìng)賽沒(méi)拿到名次、女神也沒(méi)追到的現(xiàn)在依舊如是。
線程池嘛,主要功能就是管理調(diào)度線程,節(jié)省線程重復(fù)申請(qǐng)/釋放所帶來(lái)的損耗。我們先來(lái)聊一聊現(xiàn)在 github 和 B 乎上找到的一些通用 C++ 的實(shí)現(xiàn)方法。很多的思路都很簡(jiǎn)單:一般就是提供一個(gè)接口,可以把任務(wù)塞入一個(gè) queue 中,交由線程池中的線程異步執(zhí)行,然后等待(也可以不等待)結(jié)果返回。

看上面的圖,m 個(gè)(圖中為 3 個(gè))輸入源把待執(zhí)行的任務(wù),放到一個(gè)任務(wù)隊(duì)列中,然后線程池中的 n 個(gè)(圖中為 2 個(gè))線程,依次去消費(fèi)這個(gè) queue。這是最基礎(chǔ)的版本,最簡(jiǎn)單的模型,同時(shí)問(wèn)題也有很多。
你看哈,我們用兩個(gè)線程處理 queue 中的任務(wù),如果這一刻任務(wù)數(shù)量忽然暴增,我們還只用兩個(gè)線程處理么?或者說(shuō),長(zhǎng)時(shí)間隊(duì)列都是空的,我們還保留兩個(gè)線程么?
你再看哈,在 input 的時(shí)候,隊(duì)列尾部是需要加鎖的,不然多個(gè)輸入源,不就亂套了么。再講究一點(diǎn)要判斷是否超出規(guī)定大小啥的。當(dāng)有任務(wù)輸入的時(shí)候,需要發(fā)送一個(gè)信號(hào)去通知 pool 中的線程:來(lái)接客了。哦,不對(duì),github 上的說(shuō)法是:來(lái)處理任務(wù)了。
這個(gè)時(shí)候,沒(méi)有在處理任務(wù)的 thread 就會(huì)去從 queue 的頭部,獲取一個(gè)任務(wù)然后執(zhí)行了。注意,這個(gè)過(guò)程也是需要加鎖的(對(duì)應(yīng)圖中 T0 下面的那個(gè)紅色的 Lock)。當(dāng)然了,這其中還會(huì)涉及到如果 queue 為空時(shí)候的等待處理。
我們?cè)O(shè)想一種情況,此刻 pool 中一共有 5 個(gè) thread 可以運(yùn)行,而 queue 中有 3 個(gè)任務(wù),分別是 Task0,Task1,Task2 吧。會(huì)發(fā)生什么,這 5 個(gè) thread 去爭(zhēng)搶 Task0 的執(zhí)行權(quán),其中有一個(gè)拿到了,滿意的去執(zhí)行了。然后接下來(lái) 4 個(gè) thread 再去爭(zhēng)搶 Task1 任務(wù)的執(zhí)行權(quán),一個(gè)成功之后,剩下的 3 個(gè) thread 再去爭(zhēng)搶 Task2 的執(zhí)行權(quán),over。
我再這里說(shuō)【爭(zhēng)搶】,其實(shí)就是搶鎖的一個(gè)過(guò)程,因?yàn)閺?queue 中獲取任務(wù)是需要上鎖的,這個(gè)過(guò)程是有等待損耗的。而且,理論上這種one by one的同步,相對(duì)是很耗時(shí)的。
基本上所有提升并發(fā)的優(yōu)化,都是有兩個(gè)最基本出發(fā)點(diǎn):一個(gè)是增加扇入扇出,一個(gè)是增加負(fù)載。這話不是我說(shuō)的哈,是一位不愿意透露所在公司的阿里云高 P 大佬說(shuō)的。翻譯過(guò)來(lái)就是批量進(jìn)出,批量執(zhí)行??赡芨唠A一些的還會(huì)提到命中緩存、綁定執(zhí)行 cpu 啥的,那基本上不是純并行處理的范圍(或者說(shuō),也是批量執(zhí)行的一部分)。
設(shè)定目標(biāo)
做一個(gè)東西之前,總要給自己設(shè)定一點(diǎn)目標(biāo)。否則做著做著可能就跑偏了,最后寫(xiě)出來(lái)個(gè)內(nèi)存池或連接池也說(shuō)不定。

我們先把 flag 定下來(lái) :
?開(kāi)箱即用:基于 std 庫(kù)純手工實(shí)現(xiàn),兼容 mac/linux/windows 跨平臺(tái)使用,無(wú)任何第三方依賴。?上手簡(jiǎn)單:無(wú)腦往 threadpool 中塞任務(wù)即可,支持任意格式(入?yún)?、返回值)的任?wù)(函數(shù))執(zhí)行。?性能優(yōu)異:盡可能減少調(diào)度過(guò)程中各種細(xì)節(jié)損耗(比如,new、copy 構(gòu)造等)。性能測(cè)試可以看出,調(diào)度性能明顯優(yōu)于 github 上搜到的排在首頁(yè)的 C++的線程池。?功能強(qiáng)大:各種功能參數(shù)開(kāi)放設(shè)置。功能層面向 Java 版本看齊(這話說(shuō)明暫時(shí)還沒(méi)對(duì)齊,懂的都懂)。?穩(wěn)定可靠:經(jīng)歷過(guò)億次級(jí)別的測(cè)試,功能穩(wěn)定正?!獩](méi)想到吧,我不但會(huì)寫(xiě)(B)代(U)碼(G),還會(huì)自測(cè)。
本章小結(jié)
這一節(jié),主要介(tu)紹(cao)了 C++生態(tài)中對(duì)線程池的支持力度的不足,講了我在寫(xiě)CGraph的過(guò)程中,針對(duì)線程池優(yōu)化的一些目標(biāo)和思路。暫時(shí)還沒(méi)說(shuō)到具體的實(shí)現(xiàn)方式——這個(gè)將會(huì)在下一章中跟大家介紹。歡迎大家繼續(xù)關(guān)注哈。
當(dāng)然了,如果大家有什么好的思路和意見(jiàn),也很歡迎大家提出來(lái),或者幫忙實(shí)現(xiàn)一下。這樣才能共同進(jìn)步嘛,期待您的指教。Build together! Power another!
推薦閱讀
?純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——執(zhí)行邏輯[3]?純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——循環(huán)邏輯[4]?純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——參數(shù)傳遞[5]?純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——條件判斷[6]
引用鏈接
[1] 個(gè)人博客鏈接: http://www.chunel.cn[2] CGraph 源碼鏈接: https://github.com/ChunelFeng/CGraph[3] 純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——執(zhí)行邏輯: http://www.chunel.cn/archives/cgraph-run-introduce[4] 純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——循環(huán)邏輯: http://www.chunel.cn/archives/cgraph-loop-introduce[5] 純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——參數(shù)傳遞: http://www.chunel.cn/archives/cgraph-param-introduce[6] 純序員給你介紹圖化框架的簡(jiǎn)單實(shí)現(xiàn)——條件判斷: http://www.chunel.cn/archives/cgraph-condition-introduce
