:手寫線程池與定時器)
0. 為什么需要線程池每次任務(wù)來都std::thread新建線程的代價創(chuàng)建成本高內(nèi)核要分配 PCB、棧、調(diào)度結(jié)構(gòu)約幾十~上百微秒。數(shù)量失控突發(fā)流量下線程數(shù)爆炸上下文切換context switch開銷反超業(yè)務(wù)。無管理無法限制并發(fā)度、無法隊列化背壓。線程池的核心思想預(yù)先起固定數(shù)量的 worker 線程任務(wù)以std::function形式入隊worker 從隊列取任務(wù)執(zhí)行。達(dá)到線程復(fù)用 并發(fā)可控 隊列削峰。1. 最簡線程池條件變量版#includethread#includevector#includequeue#includemutex#includecondition_variable#includefunctional#includeiostreamclassThreadPool{public:explicitThreadPool(size_t n):stop_(false){for(size_t i0;in;i)workers_.emplace_back([this]{workerLoop();});}~ThreadPool(){{std::lock_guardstd::mutexlk(mtx_);stop_true;}cv_.notify_all();// 喚醒所有 worker 讓其退出for(autot:workers_)t.join();// 等線程結(jié)束RAII 析構(gòu)}// 提交任務(wù)無返回值版voidsubmit(std::functionvoid()task){{std::lock_guardstd::mutexlk(mtx_);tasks_.push(std::move(task));}cv_.notify_one();// 喚醒一個空閑 worker}private:voidworkerLoop(){while(true){std::functionvoid()task;{std::unique_lockstd::mutexlk(mtx_);// 用 while條件變量避免虛假喚醒stop_ 時無論隊列空否都退出cv_.wait(lk,[this]{returnstop_||!tasks_.empty();});if(stop_tasks_.empty())return;taskstd::move(tasks_.front());tasks_.pop();}task();// 在鎖外執(zhí)行減小臨界區(qū)}}std::vectorstd::threadworkers_;std::queuestd::functionvoid()tasks_;std::mutex mtx_;std::condition_variable cv_;boolstop_;};設(shè)計要點cv_.wait(lk, pred)的pred必須用while語義判斷條件變量可能被虛假喚醒謂詞里包含stop_保證退出信號也能喚醒。任務(wù)在鎖外執(zhí)行task()在unique_lock析構(gòu)解鎖之后調(diào)用避免持鎖運行導(dǎo)致 worker 間串行化這是性能關(guān)鍵。析構(gòu)函數(shù)先置stop_再notify_all再join保證所有 worker 干凈退出不丟任務(wù)隊列里剩余任務(wù)不再執(zhí)行生產(chǎn)可改為drain 模式。2. 讓 submit 有返回值std::future無返回值的池只能發(fā)了不管。用std::packaged_task把任務(wù)結(jié)果打包進(jìn)std::future提交即可get()等待結(jié)果#includefuturetemplateclassFautosubmitWithResult(Ff)-std::futuredecltype(f()){usingRdecltype(f());autotaskstd::make_sharedstd::packaged_taskR()(std::forwardF(f));std::futureRfuttask-get_future();{std::lock_guardstd::mutexlk(mtx_);// packaged_task 不可拷貝用 lambda 包一層 shared_ptrtasks_.push([task]{(*task)();});}cv_.notify_one();returnfut;}用法auto r pool.submitWithResult([]{ return 12; }); r.get(); // 3。注意packaged_task不可拷貝必須存shared_ptr否則push進(jìn)std::function會編譯失敗——這是高頻坑。3. 任務(wù)異常怎么辦若task()拋異常異常會在future.get()處重新拋出因為packaged_task捕獲了異常進(jìn) future。但無返回值版submit直接task()拋異常會終止整個 worker 線程std::thread 析構(gòu)時std::terminate。生產(chǎn)級做法worker 里包一層try/catch或統(tǒng)一要求任務(wù)自身不拋異常。面試常問任務(wù)拋異常會怎樣——務(wù)必答清兩條路徑差異。4. 線程數(shù)怎么定經(jīng)典經(jīng)驗公式CPU 密集型線程數(shù) ≈ 核數(shù)std::thread::hardware_concurrency()多了只增加切換。I/O 密集型線程數(shù)可遠(yuǎn)大于核數(shù)公式 ≈ 核數(shù) × (1 等待時間/計算時間)。靠壓測調(diào)優(yōu)。更高級的做法是分離線程池CPU 池核數(shù)個 I/O 池較多避免慢 I/O 阻塞計算任務(wù)。5. 無鎖任務(wù)隊列進(jìn)階std::queue mutex在超高并發(fā)提交時會成為瓶頸??捎肕PMC多生產(chǎn)者多消費者無鎖隊列如 moodycamel::ConcurrentQueue或自己用 atomic 實現(xiàn)。核心思想用 CAS 維護(hù)頭尾指針、每個節(jié)點獨立、避免全局鎖。本篇不展開完整 MPMC 實現(xiàn)見 S04 無鎖環(huán)形隊列的 SPSC 思路但面試要知道高吞吐線程池可換無鎖隊列。6. 定時器最小堆實現(xiàn)服務(wù)器需要300ms 后檢查連接是否存活“每 5s 上報一次”。用**最小堆按到期時間排序**管理定時任務(wù)由一個 timer 線程wait_until(最近到期時刻)到點執(zhí)行并重新入堆#includechrono#includequeue#includemutex#includecondition_variable#includefunctional#includethreadstructTimerTask{usingClockstd::chrono::steady_clock;Clock::time_point expire;std::functionvoid()cb;uint64_tid;booloperator(constTimerTasko)const{returnexpireo.expire;}// 小頂堆};classTimerHeap{public:uint64_tadd(std::chrono::milliseconds ms,std::functionvoid()cb){std::lock_guardstd::mutexlk(mtx_);TimerTask t{steady_clock::now()ms,std::move(cb),seq_};heap_.push(t);if(heap_.top().idt.id)cv_.notify_one();// 新任務(wù)更早到期喚醒returnt.id;}voidrun(){// 在獨立線程調(diào)用while(true){std::unique_lockstd::mutexlk(mtx_);if(heap_.empty()){cv_.wait(lk);// 無任務(wù)則等}else{autotopheap_.top();cv_.wait_until(lk,top.expire);// 等至最近到期if(heap_.empty()||heap_.top().expiresteady_clock::now())continue;autotaskheap_.top();heap_.pop();lk.unlock();task.cb();// 執(zhí)行回調(diào)鎖外lk.lock();}}}std::priority_queueTimerTask,std::vectorTimerTask,std::greaterTimerTaskheap_;std::mutex mtx_;std::condition_variable cv_;uint64_tseq_0;};核心技巧cv_.wait_until(lk, top.expire)——當(dāng)有人插入一個更早到期的任務(wù)時add里notify_one打斷等待timer 線程重新計算最近到期避免新任務(wù)雖早卻要等舊任務(wù)到點才觸發(fā)的延遲 bug。這是定時器實現(xiàn)的關(guān)鍵正確性點。7. 定時器時間輪HashedWheelTimer當(dāng)定時器數(shù)量極大十萬級如海量連接心跳最小堆的O(log N)插入與每次 wakeup 都要掃堆頂會變慢。時間輪借鑒時鐘一個環(huán)形數(shù)組槽指針按 tick 轉(zhuǎn)動每 tick 推進(jìn)一格執(zhí)行該格所有任務(wù)。超時時間 一輪的用圈數(shù)round標(biāo)記轉(zhuǎn)到對應(yīng)圈再執(zhí)行。插入O(1)、刪除O(1)適合大量短周期任務(wù)。Netty 的HashedWheelTimer即此思想。本篇給出概念骨架structWheelSlot{std::vectorTimerTasktasks;};std::vectorWheelSlotwheel_;// 如 512 槽size_t cursor_0;// tick(): cursor_ (cursor_1) % N; 執(zhí)行 wheel_[cursor_] 中 round0 的任務(wù)// round0 的任務(wù) round-- 并可能跨圈重掛面試對比堆定時器實現(xiàn)簡單、精度高、適合少量定時任務(wù)時間輪插入刪除 O(1)、適合海量定時但精度受 tick 粒度限制tick1ms 則誤差1ms。8. 實戰(zhàn)線程池 定時器 心跳模擬把兩者結(jié)合線程池執(zhí)行任務(wù)定時器每 3 秒打印一次心跳驗證并發(fā)組件協(xié)作。#includeiostream#includechronointmain(){ThreadPoolpool(4);// 提交 10 個計算任務(wù)for(inti0;i10;i)pool.submit([i]{std::this_thread::sleep_for(std::chrono::milliseconds(100));std::couttask i done on std::this_thread::get_id()\n;});TimerHeap timer;std::threadtimer_thread([]{timer.run();});timer.add(std::chrono::seconds(3),[]{std::cout[heartbeat] alive\n;});timer.add(std::chrono::seconds(6),[]{std::cout[heartbeat] alive\n;});std::this_thread::sleep_for(std::chrono::seconds(7));// 真實項目里用原子 stop 標(biāo)志優(yōu)雅退出 timer 線程return0;}編譯g -stdc17 pool.cpp -lpthread。你會看到 10 個 task 被 4 個 worker 復(fù)用執(zhí)行定時器在 3s/6s 各觸發(fā)一次。9. 與高性能服務(wù)器結(jié)合真實網(wǎng)絡(luò)服務(wù)器如基于 epoll/io_uring通常用一個 accept 線程N 個 worker 線程每個跑一個事件循環(huán)即one loop per thread見 libevent/libev/muduo 思想。定時器集成進(jìn)事件循環(huán)epoll 用timerfdio_uring 用IORING_OP_TIMEOUT而非獨立線程——避免跨線程喚醒。計算密集任務(wù)才丟進(jìn)獨立線程池防止阻塞 I/O 事件循環(huán)。這是事件循環(huán) 線程池混合模型的典型架構(gòu)面試常問為什么不能所有活都在事件循環(huán)線程做——答案長耗時任務(wù)會拖慢所有連接的事件處理必須卸載offload到線程池。10. 速答 12 題臨場背誦線程池核心組成一組 worker 線程 任務(wù)隊列 同步機制mutex/condvar 或無鎖隊列。為什么任務(wù)要在鎖外執(zhí)行防止持鎖運行導(dǎo)致 worker 串行化最大化并發(fā)。條件變量為什么用 while 判斷防虛假喚醒且 stop_ 也要能喚醒退出。submit 返回 future 怎么實現(xiàn)packaged_task包任務(wù)存shared_ptr進(jìn)隊列g(shù)et_future()返回。packaged_task 為何要 shared_ptr它不可拷貝push 進(jìn)std::function需共享所有權(quán)。任務(wù)拋異常會怎樣有 future 版在 get() 重拋無返回值版會 terminate worker需 try/catch。CPU 密集 vs I/O 密集線程數(shù)前者≈核數(shù)后者可遠(yuǎn)大于核數(shù)靠壓測定。最小堆定時器插入復(fù)雜度O(log N)取最近到期 O(1)。wait_until 的作用timer 線程等到最近到期新更早任務(wù)插入時 notify 打斷重算。時間輪適用場景海量十萬級定時任務(wù)插入刪除 O(1)精度受 tick 限制。為什么長任務(wù)不能放事件循環(huán)線程會阻塞所有連接事件處理應(yīng) offload 到線程池。epoll 定時器怎么實現(xiàn)timerfd注冊進(jìn) epoll超時即可讀事件。11. 臨場口訣池子復(fù)用線程隊列削峰限并發(fā)任務(wù)鎖外跑future 包結(jié)果堆定時器按點到時間輪海量快長活別占事件環(huán)卸載線程池才穩(wěn)。12. 生產(chǎn)級增強方向基礎(chǔ)版線程池能跑但離生產(chǎn)還差幾步優(yōu)雅停機drain 模式基礎(chǔ)析構(gòu)直接丟棄隊列剩余任務(wù)。生產(chǎn)應(yīng)提供shutdown(waittrue)置stop_后先處理完隊列中已有任務(wù)再退出drain或shutdownNow立即丟棄。結(jié)合std::atomicbool而非裸bool避免數(shù)據(jù)競爭。工作竊取work-stealing單一全局隊列在高并發(fā)提交時成為鎖熱點。更高階做法是為每個 worker 配一個雙端隊列deque自己從頭取、其他 worker 從尾部偷——std::deque 每線程獨立 mutex或參照 Intel TBB /folly::MPMC實現(xiàn)。這把集中鎖分散為局部鎖 偶爾偷取吞吐更高。線程親和性把 worker 綁到特定 CPUpthread_setaffinity_np減少跨核緩存失效配合 NUMA 把內(nèi)存分配靠近線程所在節(jié)點。動態(tài)擴容固定線程數(shù)簡單但 I/O 阻塞時可能不夠。可設(shè)核心池 臨時線程空閑超時回收類似CachedThreadPool但要防線程數(shù)爆炸——加上限與空閑回收。13. 線程池常見坑面試必考任務(wù)在鎖內(nèi)執(zhí)行若task()在持mtx_時運行所有 worker 被串行化線程池退化為單線程。必須鎖外執(zhí)行見第 1 節(jié)。析構(gòu)時任務(wù)拋異常無返回值版submit的task()若拋異常會std::terminate整個進(jìn)程。生產(chǎn)須包try/catch或要求任務(wù) noexcept。重復(fù) join / 在 worker 內(nèi) submit 自身池在 worker 線程里向同一個池submit并future.get()會死鎖線程被占用無人執(zhí)行該任務(wù)。解決辦法使用std::async(std::launch::async)或獨立的繼續(xù)continuation機制而非同池阻塞等待。stop_ 非原子用std::atomicbool stop_而非裸bool否則編譯器優(yōu)化可能讓 worker 看不到更新。notify 遺漏向空隊列 notify 無副作用但若submit在push前 notify 會丟喚醒。務(wù)必先push后notify。線程數(shù)大于任務(wù)數(shù)導(dǎo)致空轉(zhuǎn)worker 全阻塞在cv_.wait無任務(wù)時仍占內(nèi)存長時間空閑可考慮超時回收。14. 定時器取消與層級時間輪真實服務(wù)器常需要取消定時器如連接正常關(guān)閉不必再發(fā)心跳。最小堆取消需id - 堆內(nèi)位置的額外映射如std::unordered_mapid, 堆索引 惰性刪除標(biāo)記。時間輪取消則是從對應(yīng)槽移除節(jié)點O(1)更易實現(xiàn)。當(dāng)定時跨度極大從毫秒到數(shù)小時單輪時間輪槽數(shù)爆炸改用層級時間輪hierarchical wheel類似時鐘的秒針、分針、時針低精度輪溢出時把任務(wù)升級到高精度輪。Linux 內(nèi)核timer wheel與 NettyHashedWheelTimer都基于此。面試答海量 長跨度定時時應(yīng)主動提到層級時間輪體現(xiàn)深度。15. 與可觀測性結(jié)合線程池與定時器是線上故障高發(fā)點任務(wù)堆積隊列越來越長、某 worker 卡死線程長時間 100%、定時器泄漏不斷 add 不 cancel。建議暴露指標(biāo)pending_tasks隊列長度、active_threads、completed_total。用 S03 的 perf /perf trace抓長時間運行的任務(wù)用gdb對卡死 worker 抓棧見 S09。定時器加上限監(jiān)控防泄漏。把這些寫完組件 能觀測 能排查連起來正是 S01–S09 全系列想訓(xùn)練的工程閉環(huán)。16. 定時器融入事件循環(huán)timerfd epoll第 6、7 節(jié)的定時器用獨立 timer 線程。但在 epoll 事件循環(huán)S01里更地道的是用timerfd創(chuàng)建一個定時器 fd把它EPOLL_CTL_ADD進(jìn) epoll超時后 epoll 報可讀讀一下即可觸發(fā)到期任務(wù)。這樣定時器與網(wǎng)絡(luò) I/O 共用一個事件循環(huán)無需跨線程喚醒inttfdtimerfd_create(CLOCK_MONOTONIC,0);structitimerspecits{};its.it_value{3,0};// 3 秒后首次its.it_interval{3,0};// 之后每 3 秒timerfd_settime(tfd,0,its,nullptr);epoll_ctl(epfd,EPOLL_CTL_ADD,tfd,ev);// 注冊// 事件循環(huán)里tfd 可讀 → read(tfd,...) 消耗通知 → 執(zhí)行心跳這種一切皆 fd、統(tǒng)一進(jìn) epoll的思想和 io_uring 的IORING_OP_TIMEOUT異曲同工——都是把定時器變成事件源。面試若被問怎么在單線程事件循環(huán)里做定時答timerfdepoll 系或OP_TIMEOUTio_uring 系即可。17. 線程池 vs 協(xié)程C20C20 引入無棧協(xié)程co_await/task它和線程池解決不同層面的問題線程池解決并行執(zhí)行多個獨立任務(wù)、控制并發(fā)度任務(wù)間切換由 OS 調(diào)度開銷是線程級。協(xié)程解決單個任務(wù)內(nèi)部在 I/O 等待時讓出不阻塞線程切換是用戶態(tài)、開銷極小適合海量連接各自順序?qū)戇壿嫷膱鼍耙粋€連接一個協(xié)程看起來像同步代碼實則異步?,F(xiàn)代框架如 cppcoro、asio 的 awaitable把協(xié)程跑在線程池的 worker 上線程池提供并行度協(xié)程提供異步不阻塞線程的寫法。兩者不是替代而是互補。面試答協(xié)程會不會取代線程池——答協(xié)程是任務(wù)內(nèi)部掛起機制線程池是并行執(zhí)行載體常結(jié)合使用體現(xiàn)體系化認(rèn)知。18. 阻塞隊列自測題附線程池的底層是線程安全的任務(wù)隊列。??际謱懸粋€阻塞隊列templateclassTclassBlockingQueue{std::queueTq_;std::mutex m_;std::condition_variable cv_;booldone_false;public:voidpush(T v){std::lock_guardstd::mutexlk(m_);q_.push(std::move(v));cv_.notify_one();}boolpop(Tout){// 返回 false 表示已關(guān)閉std::unique_lockstd::mutexlk(m_);cv_.wait(lk,[this]{returndone_||!q_.empty();});if(q_.empty())returnfalse;// done_ 且空outstd::move(q_.front());q_.pop();returntrue;}voidclose(){std::lock_guardstd::mutexlk(m_);done_true;cv_.notify_all();}};這其實就是線程池任務(wù)隊列的精簡版掌握它線程池就通了。19. 速查并發(fā)組件決策表場景推薦方案理由少量定時心跳、超時最小堆定時器實現(xiàn)簡單、精度高、O(logN) 可接受海量定時十萬級連接時間輪 / 層級時間輪插入刪除 O(1)tick 粒度控精度單線程事件循環(huán)里做定時timerfd epoll一切皆 fd統(tǒng)一事件源CPU 密集批量任務(wù)固定線程池核數(shù)個避免切換吃滿算力I/O 密集任務(wù)線程池核數(shù)用等待時間換并發(fā)度長耗時任務(wù)怕阻塞 I/O卸載到獨立線程池保護(hù)事件循環(huán)不被拖垮需要任務(wù)返回值submit 返回 futurepackaged_task 接異常與結(jié)果高頻小回調(diào)、不存儲function_ref零開銷、非擁有、免分配這張決策表把前面所有點收斂成什么時候用什么。面試被問你怎么選定時方案/線程數(shù)時按表逐條給出權(quán)衡比背定義得分高得多。記住鐵律事件循環(huán)只做 I/O 調(diào)度重活卸載線程池少量定時用堆、海量定時用輪熱路徑回調(diào)用 ref 而非 function。20. 臨場自檢三問并發(fā)組件“為什么任務(wù)要在鎖外執(zhí)行”—— 若持鎖運行task()所有 worker 被串行化線程池退化成單線程鎖只保護(hù)隊列臨界區(qū)越小并發(fā)越高?!岸〞r器用堆還是輪”—— 少量定時用最小堆簡單、O(logN)、精度高海量十萬級用時間輪插入刪除 O(1)精度受 tick 限制單線程事件循環(huán)用 timerfd 統(tǒng)一進(jìn) epoll?!伴L任務(wù)能放事件循環(huán)線程嗎”—— 不能會阻塞所有連接事件處理必須卸載到線程池保護(hù) I/O 調(diào)度不被拖垮。這三條是線程池與定時器面試的高頻收口問題能倒背如流即過關(guān)。動手建議把第 1 節(jié)的線程池與第 8 節(jié)的定時器組合編譯運行g(shù) -stdc17 -pthread故意提交一個會拋異常的任務(wù)觀察 worker 是否被terminate再給 TimerHeap 插入一個比當(dāng)前堆頂更早到期的任務(wù)驗證wait_until是否被正確喚醒。踩過這些坑你講出來的線程池才是有血有肉的工程經(jīng)驗而不是教科書定義。同時把這些組件與 S01 的 epoll、S05 的 io_uring 結(jié)合起來思考事件循環(huán)負(fù)責(zé) I/O 調(diào)度線程池負(fù)責(zé)計算卸載定時器負(fù)責(zé)超時與心跳三者拼成一臺完整的高并發(fā)服務(wù)器。記住線程池與定時器的價值不在能跑而在并發(fā)可控、任務(wù)可觀測、停機可優(yōu)雅——這三點是生產(chǎn)級組件與玩具的分界線。