<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手擼一款簡單高效的線程池(三)—— 性能優(yōu)化!

          共 5408字,需瀏覽 11分鐘

           ·

          2021-08-16 05:39

          在上一章中,我們給大家介紹了一些 C++線程池中的優(yōu)化思路和實現(xiàn)方案。這一章中,我們將繼續(xù)這個主題,接著聊線程池中還有可以“壓榨”的空間。為實現(xiàn)我們吹過的牛 B,而繼續(xù)編程

          大家好,我是不會寫代碼的純序員——Chunel Feng。上一章中,我們主要聊到了線程池中的 thread-local 機制、lock-free 機制和 work-stealing 機制。今天,繼續(xù)說說線程池優(yōu)化的一些方法和實踐,主要會涉及到:自動擴縮容機制、批量處理機制和負載均衡機制

          還是那句話,以下這些內(nèi)容,僅局限于我個人的認知。如果有什么不對或者不合理的地方,很歡迎大家隨時批評指正,也很希望大家可以提出自己意見、建議和看法。

          首先,還是照例,先上源碼鏈接:CGraph 源碼鏈接[1]。其中,線程池的實現(xiàn)在 /src/UtilsCtrl/ThreadPool/ 文件夾中

          自動擴縮容機制

          上一章,我們聊了一個問題:如果線程池中,忽然來了很多任務(wù) (比如:吳亦凡出事了,大家都來微博瘋狂圍觀點贊),這個時候怎么辦?那過了一陣子,又長時間沒有任務(wù)可執(zhí)行了 (比如:吳亦凡微博被官方屏蔽了)怎么辦?還能怎么辦,當然是心疼凡凡三秒鐘了!!!

          一個很通用的思路,就是自動擴縮容機制

          翻譯過來,就是:在任務(wù)繁忙的時候,pool 中多加入幾個 thread;而在清閑的時候,對 thread 進行自動回收CGraph的實現(xiàn)邏輯中,包含了兩種線程:PrimaryThread(主線程,簡稱PT)和 SecondaryThread(輔助線程,簡稱ST),默認在程序運行的時候,啟動CGRAPH_DEFAULT_THREAD_SIZE個 PT 去執(zhí)行任務(wù)。在程序運行的過程中,PT 的數(shù)量是恒定不變的,增/減的僅可能是 ST——這一點,是參考 Java 中 ThreadPool 的機制。

          如何判斷 pool 是忙還是閑?可以使用 running 標記的方法 + TTL(time to live)計數(shù)的方法。除了 PT 和 ST,pool 中還開辟了一個 MonitorThread(監(jiān)控線程,簡稱MT)。MT 每隔固定的時間,會去輪詢監(jiān)測所有的 PT 是否都在 running 狀態(tài)。如果是,就認定當前 pool 處于忙碌狀態(tài),則添加一個 ST 幫忙分擔(dān)任務(wù)執(zhí)行。同樣的,MT 還會去監(jiān)測每個 ST 的狀態(tài)。如果連續(xù) TTL 次監(jiān)測到 ST 沒有在執(zhí)行任務(wù),則認為 pool 處于空閑狀態(tài),則會銷毀當前 ST。

          這樣就做到了線程數(shù)量隨著 pool 的忙碌和空閑,動態(tài)調(diào)整了。當然,ST 的數(shù)量也不會無限增加的。當 PT 和 ST 的數(shù)量之和,達到CGRAPH_MAX_THREAD_SIZE值的時候,ST 數(shù)量就只減不增了。

          千萬要避免的一個誤區(qū)是:并不是線程開辟的越多,性能就越好

          線程調(diào)度過程也是有損耗的。比如,在 pool 中開辟了 1 爽(約 200+w)的線程,cpu 就需要通過調(diào)度算法,來盡可能的保障每個線程都有時間片可以運行。那這其中線程來回切換的成本是很高的——很可能遠高于任務(wù)執(zhí)行的成本。講通俗一點就是:一個和尚挑水喝,兩個和尚抬水喝,200w 個和尚沒水喝

          在其他文章中,也看到過一些意見,比如:

          ?計算密集型任務(wù),開辟nn+1個(其中,n=cpu 核數(shù))線程數(shù)?IO 密集型任務(wù),開辟2n+1個線程

          也看到有童鞋列出來一個公式,根據(jù)任務(wù)的耗時等參數(shù),來預(yù)估合理的線程數(shù)量。類似:

          ?最佳線程數(shù)目 = ((線程等待時間+線程 CPU 時間)/線程 CPU 時間 )* CPU 數(shù)目

          我想說的是,這些有道理,也值得參考。但適合的配置參數(shù),最好還是帶入實際任務(wù)中測試和檢驗。實測,是檢驗性能的唯一標準。Talk is cheap, show me the test.

          PT 和 ST 功能上的一些異同

          首先說明,PT 和 ST 都是線程,都可以執(zhí)行插入的任務(wù)。不過,CGraph 的線程池源碼中,給 PT 和 ST 做出一些差別對待。PT 中有自己的任務(wù)隊列,執(zhí)行 task 的順序依次是:local -> pool -> steal,即:先執(zhí)行自己任務(wù)隊列中的 task;如果自身的 queue 為空,則執(zhí)行 pool 中的 task;如果仍沒有任務(wù),就從相鄰的 n 個 PT 中去 steal(具體邏輯請看上一章內(nèi)容)。

          /* 主線程執(zhí)行task的邏輯 */void runTask() {    UTaskWrapper task;    if (popTask(task) || popPoolTask(task) || stealTask(task)) {        is_running_ = true;        task();        is_running_ = false;    } else {        std::this_thread::yield();    }}

          而 ST 沒有屬于自己任務(wù)隊列,僅可以執(zhí)行 pool 中的 queue 的任務(wù),對應(yīng)代碼也就是popPoolTask(task)這部分。

          這樣做的原因,一方面是可以從代碼層面,突出主輔的層次感。更重要的一方面是保證了 ST 釋放的時候不容易出錯或丟失任務(wù),并且不讓 work-stealing 的過程變得更復(fù)雜。

          批量處理機制

          上一章,提到線程池優(yōu)化的一個基本出發(fā)點就是 增加扇入扇出。為了在這一點上做到更優(yōu),CGraph 在從 queue 中獲取 task 的時候,提供了批量獲取 tasks 的功能。在開啟CGRAPH_BATCH_TASK_ENABLE參數(shù)之后,PT 和 ST 在從 queue 中獲取/盜取 task 的時候,就不再是one by one的獲取,而是batch by batch的獲取。這樣做的最大好處,是可以減少線程獲取 task 時候,爭搶鎖的次數(shù),從而提升性能。

          這種機制也可能會打亂 queue 中任務(wù)的執(zhí)行順序,單獨使用的時候需要慎重考慮。但是,針對圖化框架這種情況,并沒有這種擔(dān)憂。放入線程池中執(zhí)行的任務(wù),均是“互不依賴”的,也就是“無序執(zhí)行”的——因為有依賴的節(jié)點,會等待被依賴節(jié)點執(zhí)行完畢后,才被放入 pool 中。

          即便是這樣,也不能無腦使用。設(shè)想一種情況,pool 中有 4 個 PT,任務(wù)隊列中共有 100 個sleep 1s;的任務(wù)。一個 PT 過來一批拉走了這 100 個任務(wù),然后就默默的運行了 100s,而其他所有的線程都在旁邊默默鼓掌么?

          這樣做的好處,是原先需要有 100 次的搶鎖過程被縮減到了 1 次。但是壞處嗎,原先約 25s 就可以執(zhí)行完的任務(wù),硬生生的被執(zhí)行 100s。

          /** * 從頭部開始批量獲取可執(zhí)行任務(wù)信息 * @param taskArr * @return */bool tryMultiPop(UTaskWrapperArr& taskArr) {    bool result = false;    if (mutex_.try_lock()) {        int i = CGRAPH_MAX_TASK_BATCH_SIZE;        while (!queue_.empty() && i--) {            taskArr.emplace_back(std::move(queue_.front()));            queue_.pop_front();            result = true;        }        mutex_.unlock();    }    return result;}

          為了對這樣的情況做出權(quán)衡,CGraph 中提供了CGRAPH_MAX_TASK_BATCH_SIZE參數(shù)。在開啟批量拉取功能后,每個線程每次拉取 task 的數(shù)量不能超過該數(shù)值,這樣在一定程度上在兼顧了增加扇出和減少搶鎖次數(shù)這兩個方面,又盡可能的緩解了剛才提到的那種極端情況。

          需要說明的是,真實的多線程情況遠比我們剛才討論的復(fù)雜。現(xiàn)代計算機的調(diào)度理論何其深奧和精密,我想即便是Bill GatesLinus Torvalds,甚至是Kris Wu,也無法窮盡其中奧秘。而多線程自身又有很多的不確定性。所以,具體采用哪種執(zhí)行策略,還是盡可能的去模擬真實環(huán)境進行實測和壓測,這樣得出來的結(jié)果才最有說服力——這話,我好像說了三遍了。

          負載均衡機制

          負載均衡問題,是調(diào)度方面性能優(yōu)化的一個永恒的話題。針對不同任務(wù)和不同情況,有不同的對應(yīng)策略。之所以放在最后一點說,主要是因為個人水平和見識有限,CGraph 的線程池中,在調(diào)度層面做太多的優(yōu)化。只是將一部分任務(wù)均勻的寫入 PT 的 queue 中,另一部分統(tǒng)一放到 pool 的 queue 中。但即便是這樣,也已經(jīng)比傳統(tǒng)的線程池做法,在性能上優(yōu)化了不少(關(guān)于性能測試,我會在后面的文章中介紹)。

          如果是要針對這方面做一些優(yōu)化的話,我提兩點我的看法吧:

          ?盡可能的保證當前 PT 中產(chǎn)生的 task,放入本 PT 的 queue 中執(zhí)行,也算是迎合 thread-local 的概念。?盡可能保證每個 PT 的 queue 中,任務(wù)耗時總和基本一致。不要頻繁出現(xiàn) work-stealing 的情況。

          本章小結(jié)

          本章內(nèi)容,主要跟大家介紹了線程池的一些優(yōu)化思路,包括:自動擴縮容、批量處理和負載均衡。梳理了它們在使用過程中,能夠解決的一些痛點和可能遇到的一些坑。

          面對不同數(shù)量、不同耗時、不同功能(IO 密集、計算密集)、不同順序的任務(wù)的各種實際情況,很難說有可以一招打遍天下的訣竅。很多情況下,還是需要靠實測、壓測和專業(yè)分析工具,來看出性能瓶頸點。為此,CGraph 也在源碼中開放出來了一些配置參數(shù)供大家選擇嘗試。

          推薦一個我平時用的性能監(jiān)測工具:Profile,它可以集成在CLion中一鍵執(zhí)行,并生成火焰圖。看下面這張圖,基本上一眼就可以看出來,任務(wù)執(zhí)行的耗時基本上卡在runTask()函數(shù)中。至于為什么會這樣?能否優(yōu)化?如何優(yōu)化?這些問題就要自己一點點根據(jù)代碼分析了——測試,永遠也不會告訴你答案了。

          再多說兩句,最近比較流行的協(xié)程的概念,應(yīng)該也比較適合做這種調(diào)度的場景,而且人為可控性更強。我們上面提到的一些優(yōu)化方法,也參考 Java 中的一些現(xiàn)有實現(xiàn)邏輯。如果有懂 Go 大佬可以指教一下,我想那就更好了。

          下一章,我們會給大家介紹一些 CGraph 在線程池中,做的一些工程層面的優(yōu)化,比如:減少無用 copy、提供 task 包裝器、避免 busy waiting等,希望大家繼續(xù)關(guān)注。對這些方面感興趣的朋友,也歡迎加我微信(ChunelFeng),一起隨時交流討論——添加的時候,請簡單備注一下信息哦,否則我會以為你是“茶葉小妹”呢,哈哈!

          推薦閱讀

          ?純序員給你介紹圖化框架的簡單實現(xiàn)——執(zhí)行邏輯[2]?純序員給你介紹圖化框架的簡單實現(xiàn)——循環(huán)邏輯[3]?純序員給你介紹圖化框架的簡單實現(xiàn)——參數(shù)傳遞[4]?純序員給你介紹圖化框架的簡單實現(xiàn)——條件判斷[5]?純序員給你介紹圖化框架的簡單實現(xiàn)——線程池優(yōu)化(一)[6]?純序員給你介紹圖化框架的簡單實現(xiàn)——線程池優(yōu)化(二)[7]

          引用鏈接

          [1] CGraph 源碼鏈接: https://github.com/ChunelFeng/CGraph
          [2] 純序員給你介紹圖化框架的簡單實現(xiàn)——執(zhí)行邏輯: http://www.chunel.cn/archives/cgraph-run-introduce
          [3] 純序員給你介紹圖化框架的簡單實現(xiàn)——循環(huán)邏輯: http://www.chunel.cn/archives/cgraph-loop-introduce
          [4] 純序員給你介紹圖化框架的簡單實現(xiàn)——參數(shù)傳遞: http://www.chunel.cn/archives/cgraph-param-introduce
          [5] 純序員給你介紹圖化框架的簡單實現(xiàn)——條件判斷: http://www.chunel.cn/archives/cgraph-condition-introduce
          [6] 純序員給你介紹圖化框架的簡單實現(xiàn)——線程池優(yōu)化(一): http://www.chunel.cn/archives/cgraph-threadpool-1-introduce
          [7] 純序員給你介紹圖化框架的簡單實現(xiàn)——線程池優(yōu)化(二): http://www.chunel.cn/archives/cgraph-threadpool-2-introduce




          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕一区二区三区四区五区 | 大白屁股日本女人视频 | 国产一级a毛一级a看免费 | 狠狠干中文字幕 | 亚洲成人视频免费观看 |