C++11多線程編程實(shí)戰(zhàn):從std::thread入門到線程池構(gòu)建
1. 從“單兵作戰(zhàn)”到“協(xié)同作戰(zhàn)”為什么我們需要多線程如果你寫過(guò)一些C程序尤其是涉及到界面響應(yīng)、網(wǎng)絡(luò)通信或者大量數(shù)據(jù)處理時(shí)大概率遇到過(guò)這樣的場(chǎng)景程序在執(zhí)行一個(gè)耗時(shí)操作比如讀取一個(gè)大文件、進(jìn)行復(fù)雜的科學(xué)計(jì)算時(shí)整個(gè)界面“卡死”了鼠標(biāo)點(diǎn)不動(dòng)鍵盤沒反應(yīng)仿佛程序已經(jīng)崩潰。實(shí)際上它只是在“專心致志”地做那件事無(wú)暇他顧。這就是典型的單線程程序的局限——所有任務(wù)都在一條“執(zhí)行流”上排隊(duì)一個(gè)任務(wù)沒干完后面的都得等著。多線程就是為了解決這個(gè)問(wèn)題而生的。你可以把它想象成把一個(gè)“單兵作戰(zhàn)”的程序改造成一個(gè)“協(xié)同作戰(zhàn)”的小隊(duì)。主線程比如負(fù)責(zé)界面更新的線程繼續(xù)流暢地響應(yīng)用戶操作而把那些耗時(shí)的、阻塞性的任務(wù)比如文件I/O、網(wǎng)絡(luò)請(qǐng)求、后臺(tái)計(jì)算交給新創(chuàng)建的“工作線程”去處理。這樣一來(lái)用戶界面保持流暢后臺(tái)任務(wù)也得以并行執(zhí)行程序的效率和用戶體驗(yàn)都得到了質(zhì)的提升。C在C11標(biāo)準(zhǔn)之前多線程編程是一個(gè)“各自為戰(zhàn)”的領(lǐng)域。開發(fā)者需要依賴操作系統(tǒng)提供的原生API如Windows的CreateThread Linux的pthread_create這不僅代碼不可移植而且極易出錯(cuò)尤其是在處理線程同步、數(shù)據(jù)競(jìng)爭(zhēng)這些棘手問(wèn)題時(shí)。C11將多線程支持納入了標(biāo)準(zhǔn)庫(kù)提供了std::thread、std::mutex、std::condition_variable等一系列組件終于讓C多線程編程有了統(tǒng)一的、可移植的“官方武器庫(kù)”。今天我們就來(lái)深入聊聊這個(gè)武器庫(kù)中最核心的“士兵”——std::thread。2.std::thread的誕生與基本使用創(chuàng)建你的第一個(gè)線程std::thread位于thread頭文件中。創(chuàng)建一個(gè)線程本質(zhì)上就是告訴系統(tǒng)“這里有一段獨(dú)立的代碼請(qǐng)開辟一條新的執(zhí)行路徑去運(yùn)行它”。創(chuàng)建線程最直接的方式就是將一個(gè)可調(diào)用對(duì)象函數(shù)、函數(shù)指針、Lambda表達(dá)式、函數(shù)對(duì)象傳遞給std::thread的構(gòu)造函數(shù)。2.1 使用普通函數(shù)創(chuàng)建線程這是最直觀的方式。假設(shè)我們有一個(gè)簡(jiǎn)單的任務(wù)函數(shù)#include iostream #include thread #include chrono void backgroundTask(int id, int duration) { std::cout 線程 id 開始工作預(yù)計(jì)耗時(shí) duration 秒。 std::endl; std::this_thread::sleep_for(std::chrono::seconds(duration)); // 模擬耗時(shí)操作 std::cout 線程 id 工作完成 std::endl; } int main() { std::cout 主線程開始ID: std::this_thread::get_id() std::endl; // 創(chuàng)建線程t1執(zhí)行backgroundTask傳入?yún)?shù)1和2 std::thread t1(backgroundTask, 1, 2); // 創(chuàng)建線程t2執(zhí)行backgroundTask傳入?yún)?shù)2和4 std::thread t2(backgroundTask, 2, 4); std::cout 主線程已啟動(dòng)兩個(gè)工作線程繼續(xù)執(zhí)行其他任務(wù)... std::endl; // 主線程可以在這里做其他事情 // 等待線程t1和t2執(zhí)行完畢 t1.join(); t2.join(); std::cout 所有工作線程已結(jié)束主線程退出。 std::endl; return 0; }運(yùn)行這段代碼你會(huì)看到類似以下的輸出順序可能不同這正是多線程不確定性的體現(xiàn)主線程開始ID: 0x7ff7b3435340 主線程已啟動(dòng)兩個(gè)工作線程繼續(xù)執(zhí)行其他任務(wù)... 線程 1 開始工作預(yù)計(jì)耗時(shí) 2 秒。 線程 2 開始工作預(yù)計(jì)耗時(shí) 4 秒。 線程 1 工作完成 線程 2 工作完成 所有工作線程已結(jié)束主線程退出。關(guān)鍵點(diǎn)解析構(gòu)造即啟動(dòng)std::thread t1(func, args...);這行代碼一旦執(zhí)行新線程就會(huì)立即開始執(zhí)行func(args...)。線程的調(diào)度由操作系統(tǒng)決定因此t1和t2誰(shuí)先開始執(zhí)行是不確定的。join()的必要性join()成員函數(shù)會(huì)阻塞調(diào)用它的線程這里是主線程直到對(duì)應(yīng)的線程t1或t2執(zhí)行完畢。這是極其重要的一步。如果一個(gè)std::thread對(duì)象在析構(gòu)時(shí)其關(guān)聯(lián)的線程仍然是“可匯合”的即既沒有join也沒有detach程序會(huì)調(diào)用std::terminate()導(dǎo)致崩潰。所以你必須在線程對(duì)象生命周期結(jié)束前明確選擇join等待或detach分離。std::this_thread::sleep_for這是C11在chrono和thread中提供的便利工具用于讓當(dāng)前線程休眠指定的時(shí)間。它比傳統(tǒng)sleep函數(shù)更類型安全、更現(xiàn)代化。2.2 使用Lambda表達(dá)式創(chuàng)建線程C11的Lambda表達(dá)式讓線程創(chuàng)建變得更加靈活和簡(jiǎn)潔尤其適合定義一次性使用的簡(jiǎn)單任務(wù)。#include iostream #include thread #include vector int main() { std::vectorstd::thread workers; for (int i 0; i 5; i) { // 使用Lambda捕獲i的值并定義線程任務(wù) workers.emplace_back([i]() { std::cout 工作線程 i 正在運(yùn)行線程ID: std::this_thread::get_id() std::endl; }); } std::cout 主線程等待所有工作線程... std::endl; for (auto t : workers) { t.join(); // 等待所有線程結(jié)束 } std::cout 所有工作完成。 std::endl; return 0; }為什么推薦Lambda就地定義無(wú)需在外面單獨(dú)聲明函數(shù)代碼邏輯更緊湊。捕獲上下文通過(guò)捕獲列表[]、[]或[var]可以方便地使用外部變量。但這里隱藏著多線程編程的第一個(gè)大坑數(shù)據(jù)競(jìng)爭(zhēng)。我們稍后會(huì)詳細(xì)討論。靈活性可以輕松創(chuàng)建大量行為相似但參數(shù)不同的線程。注意關(guān)于線程IDstd::this_thread::get_id()返回一個(gè)std::thread::id類型的對(duì)象它可以輸出、比較但通常沒有直接的整數(shù)含義。它是一個(gè)輕量級(jí)的、用于標(biāo)識(shí)線程的句柄。2.3 使用可調(diào)用對(duì)象仿函數(shù)如果你有一個(gè)需要維護(hù)狀態(tài)的“任務(wù)類”使用仿函數(shù)會(huì)很合適。#include iostream #include thread class Task { public: Task(const std::string name) : name_(name) {} void operator()(int times) const { // 重載函數(shù)調(diào)用運(yùn)算符 for (int i 0; i times; i) { std::cout name_ 正在執(zhí)行第 i1 次迭代。 std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } private: std::string name_; }; int main() { Task printerA(線程A); Task printerB(線程B); // 注意這里傳遞的是printerA對(duì)象的副本 std::thread t1(std::ref(printerA), 3); // 使用std::ref傳遞引用避免拷貝 std::thread t2(printerB, 5); // 傳遞printerB的副本 t1.join(); t2.join(); // 驗(yàn)證t1修改的是printerA的副本還是原對(duì)象這里t1用了std::ref所以是原對(duì)象。 // 但我們的operator()是const的并未修改對(duì)象狀態(tài)所以此例中無(wú)區(qū)別。 return 0; }這里有一個(gè)重要細(xì)節(jié)對(duì)象拷貝問(wèn)題。當(dāng)我們將printerA傳遞給線程時(shí)默認(rèn)情況下會(huì)拷貝這個(gè)對(duì)象。如果你希望線程操作的是原對(duì)象比如要修改其內(nèi)部狀態(tài)需要使用std::ref來(lái)包裝傳遞引用。反之如果對(duì)象拷貝成本很高或不支持拷貝也需要仔細(xì)考慮傳遞方式。3. 線程的生命周期管理join、detach與異常安全創(chuàng)建線程只是第一步如何管理它的生命周期確保資源正確釋放是寫出健壯多線程程序的關(guān)鍵。3.1join()等待線程結(jié)束如前所述join()是同步操作。調(diào)用線程會(huì)阻塞直到被join的線程執(zhí)行完畢。這適用于需要等待子線程結(jié)果才能繼續(xù)的主線程邏輯。join()的典型問(wèn)題異常安全??紤]以下代碼void riskyFunction() { std::thread t([]{ /* 做一些工作 */ }); // ... 這里可能拋出異常 t.join(); // 如果上面拋異常這行可能執(zhí)行不到 }如果t.join()之前的代碼拋出了異常t將不會(huì)被join導(dǎo)致其析構(gòu)時(shí)程序終止。為了解決這個(gè)問(wèn)題我們需要利用RAII資源獲取即初始化思想。解決方案使用RAII包裝器最經(jīng)典的做法是模仿標(biāo)準(zhǔn)庫(kù)的std::lock_guard寫一個(gè)thread_guard。class thread_guard { std::thread t_; public: explicit thread_guard(std::thread t) : t_(t) {} ~thread_guard() { if (t_.joinable()) { // 必須檢查不能對(duì)已經(jīng)join或detach的線程再操作 t_.join(); } } // 禁止拷貝和賦值 thread_guard(const thread_guard) delete; thread_guard operator(const thread_guard) delete; }; void safeFunction() { std::thread t([]{ /* 做一些工作 */ }); thread_guard g(t); // 守衛(wèi)對(duì)象g其析構(gòu)函數(shù)會(huì)確保t被join // ... 即使這里拋出異常g的析構(gòu)函數(shù)也會(huì)被調(diào)用從而join t。 // 不需要顯式調(diào)用 t.join(); }C20引入了std::jthreadjoining thread它在析構(gòu)時(shí)會(huì)自動(dòng)join正是為了解決這個(gè)問(wèn)題。但在C11/14/17中我們需要自己注意或者使用類似上面的守衛(wèi)類。3.2detach()放飛線程讓其獨(dú)立運(yùn)行detach()將線程與std::thread對(duì)象分離。分離后該線程將在后臺(tái)獨(dú)立運(yùn)行其資源在線程結(jié)束時(shí)由系統(tǒng)自動(dòng)回收。調(diào)用detach后原來(lái)的std::thread對(duì)象不再代表任何線程joinable()返回false。什么時(shí)候用detach適用于“發(fā)射后不管”的后臺(tái)任務(wù)比如日志記錄、監(jiān)控心跳、定時(shí)清理等。主線程無(wú)需也不應(yīng)該等待這些任務(wù)結(jié)束。void daemonTask() { while (true) { // 通常會(huì)有退出條件 std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout 后臺(tái)守護(hù)線程還在運(yùn)行... std::endl; } } int main() { std::thread t(daemonTask); t.detach(); // 分離讓它在后臺(tái)運(yùn)行 std::cout 主線程繼續(xù)執(zhí)行守護(hù)線程已分離。 std::endl; // 主線程很快結(jié)束但守護(hù)線程可能還在運(yùn)行取決于系統(tǒng)。 // 在大多數(shù)系統(tǒng)上主線程結(jié)束會(huì)導(dǎo)致整個(gè)進(jìn)程結(jié)束所有線程包括detach的都會(huì)被終止。 // 所以這個(gè)例子中守護(hù)線程可能只輸出幾次就隨進(jìn)程結(jié)束了。 std::this_thread::sleep_for(std::chrono::seconds(3)); // 讓主線程等一下看看輸出 return 0; }警告detach的風(fēng)險(xiǎn)極大失去控制分離后你無(wú)法再與該線程同步j(luò)oin也無(wú)法強(qiáng)制終止它C標(biāo)準(zhǔn)庫(kù)沒有提供線程終止的接口。生命周期風(fēng)險(xiǎn)分離的線程可能訪問(wèn)已經(jīng)銷毀的局部變量。這是最常見的錯(cuò)誤之一。void dangerousDetach() { int localVar 42; std::thread t([localVar]() { // 捕獲了局部變量的引用 std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout localVar std::endl; // 未定義行為localVar可能已銷毀 }); t.detach(); // 主線程立即返回localVar被銷毀 } // 函數(shù)結(jié)束localVar生命周期結(jié)束資源泄漏如果分離的線程陷入死循環(huán)或長(zhǎng)時(shí)間阻塞且主線程已結(jié)束它可能成為“僵尸線程”直到進(jìn)程結(jié)束。個(gè)人經(jīng)驗(yàn)除非你非常清楚自己在做什么并且線程任務(wù)完全不依賴主線程的任何資源棧變量、堆對(duì)象等否則優(yōu)先使用join。detach應(yīng)被視為高級(jí)特性謹(jǐn)慎使用。3.3 線程的移動(dòng)語(yǔ)義std::thread不支持拷貝構(gòu)造和拷貝賦值因?yàn)橐粋€(gè)線程對(duì)象代表一個(gè)唯一的執(zhí)行線程但支持移動(dòng)語(yǔ)義。這非常有用例如將線程存入容器或者實(shí)現(xiàn)線程的所有權(quán)轉(zhuǎn)移。std::thread createThread() { return std::thread([](){ std::cout 線程在函數(shù)內(nèi)部創(chuàng)建。\n; }); } int main() { std::thread t1([]{ std::cout 線程t1。\n; }); // std::thread t2 t1; // 錯(cuò)誤不能拷貝 std::thread t2 std::move(t1); // 正確移動(dòng)構(gòu)造t1不再擁有線程 std::thread t3 createThread(); // 返回值優(yōu)化或移動(dòng)構(gòu)造 std::vectorstd::thread pool; pool.push_back(std::thread([]{ /* 任務(wù)1 */ })); pool.push_back(std::thread([]{ /* 任務(wù)2 */ })); // 必須使用std::move因?yàn)閜ush_back需要拷貝而thread不可拷貝 // 實(shí)際上emplace_back是更優(yōu)選擇如之前例子所示。 if (t1.joinable()) { // false因?yàn)榫€程所有權(quán)已移給t2 std::cout t1 is joinable.\n; } else { std::cout t1 is NOT joinable.\n; } t2.join(); t3.join(); for (auto t : pool) t.join(); return 0; }移動(dòng)語(yǔ)義使得線程可以作為資源在函數(shù)間傳遞為構(gòu)建更復(fù)雜的線程管理結(jié)構(gòu)如線程池奠定了基礎(chǔ)。4. 多線程的核心挑戰(zhàn)數(shù)據(jù)競(jìng)爭(zhēng)與同步原語(yǔ)多個(gè)線程并發(fā)訪問(wèn)共享數(shù)據(jù)且至少有一個(gè)線程在寫如果沒有正確的同步就會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)引發(fā)未定義行為。這是多線程編程中最常見、最隱蔽的Bug來(lái)源。4.1 一個(gè)典型的數(shù)據(jù)競(jìng)爭(zhēng)例子#include iostream #include thread #include vector int shared_counter 0; void increment() { for (int i 0; i 100000; i) { shared_counter; // 這不是原子操作 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout Final counter value: shared_counter std::endl; // 你期望輸出 200000但實(shí)際輸出幾乎總是小于這個(gè)值且每次運(yùn)行結(jié)果可能不同。 return 0; }shared_counter這行代碼在匯編層面通常對(duì)應(yīng)“讀取-修改-寫入”三個(gè)步驟。兩個(gè)線程可能同時(shí)讀取到同一個(gè)值比如100各自加1后寫回結(jié)果變成了101而不是102。這就是數(shù)據(jù)競(jìng)爭(zhēng)。4.2 使用互斥量std::mutex保護(hù)共享數(shù)據(jù)互斥量是最基本的同步工具。它像一把鎖一次只允許一個(gè)線程進(jìn)入被保護(hù)的代碼區(qū)域臨界區(qū)。#include iostream #include thread #include vector #include mutex int shared_counter 0; std::mutex counter_mutex; // 定義一個(gè)互斥量 void safe_increment() { for (int i 0; i 100000; i) { counter_mutex.lock(); // 加鎖 shared_counter; // 臨界區(qū) counter_mutex.unlock(); // 解鎖 } } int main() { std::thread t1(safe_increment); std::thread t2(safe_increment); t1.join(); t2.join(); std::cout Final counter value: shared_counter std::endl; // 穩(wěn)定輸出200000 return 0; }手動(dòng)lock/unlock的問(wèn)題如果臨界區(qū)代碼拋出異常或者程序員忘記調(diào)用unlock會(huì)導(dǎo)致互斥量永遠(yuǎn)無(wú)法釋放其他線程無(wú)限期等待死鎖。因此永遠(yuǎn)不要直接使用lock()和unlock()成員函數(shù)。4.3 使用RAII管理鎖std::lock_guard和std::unique_lockC11提供了RAII包裝器來(lái)自動(dòng)管理鎖的生命周期。std::lock_guardC11最簡(jiǎn)單、最常用的鎖守衛(wèi)。構(gòu)造時(shí)加鎖析構(gòu)時(shí)自動(dòng)解鎖。void safer_increment() { for (int i 0; i 100000; i) { std::lock_guardstd::mutex lock(counter_mutex); // 構(gòu)造時(shí)鎖定counter_mutex shared_counter; } // lock對(duì)象離開作用域析構(gòu)時(shí)自動(dòng)解鎖 }std::unique_lockC11比lock_guard更靈活但開銷稍大。它可以延遲加鎖、手動(dòng)加解鎖、轉(zhuǎn)移所有權(quán)并且是條件變量std::condition_variable所必需的參數(shù)。void flexible_increment() { std::unique_lockstd::mutex ulock(counter_mutex, std::defer_lock); // 延遲加鎖 for (int i 0; i 100000; i) { ulock.lock(); // 手動(dòng)加鎖 shared_counter; ulock.unlock(); // 手動(dòng)解鎖可以做一些非臨界區(qū)操作 // ... 做一些不需要鎖的操作 } }經(jīng)驗(yàn)之談99%的情況下使用std::lock_guard就足夠了。它代碼簡(jiǎn)潔不易出錯(cuò)。只有當(dāng)你需要std::condition_variable或者確實(shí)需要手動(dòng)控制鎖的時(shí)機(jī)時(shí)才使用std::unique_lock。4.4 死鎖當(dāng)多個(gè)鎖糾纏在一起死鎖通常發(fā)生在需要同時(shí)獲取多個(gè)鎖的情況下。例如線程A鎖定了互斥量M1試圖鎖定M2同時(shí)線程B鎖定了M2試圖鎖定M1。雙方都在等待對(duì)方釋放資源程序陷入僵局。死鎖產(chǎn)生的四個(gè)必要條件科克定律互斥條件持有并等待不可剝奪循環(huán)等待解決方案固定鎖的順序所有線程都按相同的全局順序如先M1后M2獲取鎖。std::mutex m1, m2; void task_ab() { std::lock_guardstd::mutex lock_a(m1); std::lock_guardstd::mutex lock_b(m2); // 順序先m1后m2 // 操作共享數(shù)據(jù)A和B } void task_ba() { std::lock_guardstd::mutex lock_a(m1); // 同樣先m1后m2 std::lock_guardstd::mutex lock_b(m2); // 操作共享數(shù)據(jù)B和A }使用std::lock一次性鎖定多個(gè)互斥量C11這是一個(gè)原子操作要么全部鎖住要么一個(gè)都不鎖避免了持有并等待的條件。void safe_task() { std::unique_lockstd::mutex lock1(m1, std::defer_lock); std::unique_lockstd::mutex lock2(m2, std::defer_lock); std::lock(lock1, lock2); // 一次性鎖定lock1和lock2避免死鎖 // 現(xiàn)在lock1和lock2都已鎖定臨界區(qū)開始 }避免嵌套鎖如果設(shè)計(jì)上允許盡量重構(gòu)代碼使得一個(gè)函數(shù)只持有一個(gè)鎖。使用層次鎖為鎖定義層次級(jí)別只允許按從高到低的順序獲取鎖。4.5 線程局部存儲(chǔ)thread_local有時(shí)我們需要的不是共享數(shù)據(jù)而是每個(gè)線程都有自己的數(shù)據(jù)副本。C11引入了thread_local關(guān)鍵字用于聲明線程局部存儲(chǔ)期變量。#include iostream #include thread thread_local int thread_specific_value 0; // 每個(gè)線程都有自己獨(dú)立的副本 void print_and_increment(const std::string thread_name) { std::cout thread_name : initial value thread_specific_value std::endl; thread_specific_value 5; // 修改只影響本線程的副本 std::cout thread_name : after increment thread_specific_value std::endl; } int main() { thread_specific_value 100; // 設(shè)置主線程的副本 std::thread t1(print_and_increment, Thread-1); std::thread t2(print_and_increment, Thread-2); t1.join(); t2.join(); std::cout Main thread value thread_specific_value std::endl; // 輸出100不受子線程影響 return 0; }輸出可能類似于Thread-1: initial value 0 Thread-2: initial value 0 Thread-1: after increment 5 Thread-2: after increment 5 Main thread value 100thread_local變量對(duì)于實(shí)現(xiàn)線程安全的單例、存儲(chǔ)線程上下文如隨機(jī)數(shù)生成器、數(shù)據(jù)庫(kù)連接非常有用。它消除了同步的需要因?yàn)閿?shù)據(jù)根本不共享。5. 線程間的通信與協(xié)作條件變量與原子操作互斥量解決了數(shù)據(jù)競(jìng)爭(zhēng)但線程間經(jīng)常需要更復(fù)雜的協(xié)作比如“等待某個(gè)條件成立”。這就是std::condition_variable的用武之地。5.1 生產(chǎn)者-消費(fèi)者模型與std::condition_variable這是一個(gè)經(jīng)典的多線程協(xié)作模型。生產(chǎn)者生產(chǎn)數(shù)據(jù)放入隊(duì)列消費(fèi)者從隊(duì)列取出數(shù)據(jù)。當(dāng)隊(duì)列空時(shí)消費(fèi)者需要等待當(dāng)隊(duì)列滿時(shí)生產(chǎn)者需要等待。#include iostream #include thread #include queue #include mutex #include condition_variable #include chrono std::queueint data_queue; std::mutex queue_mutex; std::condition_variable data_cond; // 條件變量 void producer(int id) { for (int i 0; i 5; i) { std::this_thread::sleep_for(std::chrono::milliseconds(100 * (id 1))); // 模擬生產(chǎn)耗時(shí) int data id * 100 i; { std::lock_guardstd::mutex lock(queue_mutex); data_queue.push(data); std::cout 生產(chǎn)者 id 生產(chǎn)了數(shù)據(jù): data std::endl; } // lock_guard析構(gòu)自動(dòng)釋放鎖 data_cond.notify_one(); // 通知一個(gè)等待的消費(fèi)者 } } void consumer(int id) { while (true) { std::unique_lockstd::mutex lock(queue_mutex); // wait會(huì)在阻塞前自動(dòng)釋放鎖并在被喚醒后重新獲取鎖 data_cond.wait(lock, []{ return !data_queue.empty(); }); // 等待條件隊(duì)列非空 // 條件滿足且鎖已重新獲取 int data data_queue.front(); data_queue.pop(); std::cout 消費(fèi)者 id 消費(fèi)了數(shù)據(jù): data std::endl; lock.unlock(); // 可以提前解鎖處理數(shù)據(jù)非臨界區(qū)操作 // 處理數(shù)據(jù)... if (data 400) { // 簡(jiǎn)單的退出條件 std::cout 消費(fèi)者 id 收到結(jié)束信號(hào)退出。 std::endl; break; } } } int main() { std::thread p1(producer, 1); std::thread p2(producer, 2); std::thread c1(consumer, 1); std::thread c2(consumer, 2); p1.join(); p2.join(); // 生產(chǎn)者結(jié)束后需要通知消費(fèi)者結(jié)束等待。這里用了一個(gè)簡(jiǎn)單的數(shù)據(jù)值判斷。 // 更健壯的做法是發(fā)送一個(gè)特殊的“毒丸”數(shù)據(jù)或使用一個(gè)標(biāo)志位。 data_cond.notify_all(); // 喚醒所有消費(fèi)者檢查退出條件 c1.join(); c2.join(); return 0; }std::condition_variable::wait的工作原理它接受一個(gè)std::unique_lockstd::mutex和一個(gè)可選的謂詞lambda表達(dá)式。在內(nèi)部它會(huì)先釋放鎖然后阻塞當(dāng)前線程。當(dāng)被其他線程通過(guò)notify_one()或notify_all()喚醒時(shí)它會(huì)重新獲取鎖。然后檢查謂詞如果提供了。如果謂詞返回true則wait返回線程繼續(xù)執(zhí)行。如果謂詞返回false或沒有謂詞但可能是“虛假喚醒”則再次釋放鎖并阻塞。虛假喚醒即使沒有其他線程通知等待的線程也可能被喚醒。因此必須使用帶有謂詞的wait在條件不滿足時(shí)繼續(xù)等待。wait(lock, predicate)等價(jià)于while (!predicate()) wait(lock);。notify_one()vsnotify_all()notify_one()喚醒一個(gè)正在等待該條件變量的線程具體哪個(gè)不確定。適用于只有一個(gè)線程能處理通知的情況如單消費(fèi)者。notify_all()喚醒所有正在等待該條件變量的線程。它們會(huì)競(jìng)爭(zhēng)鎖然后依次檢查條件。適用于多個(gè)線程都可能需要響應(yīng)的場(chǎng)景如多消費(fèi)者或需要所有線程都知曉狀態(tài)變化。5.2 原子操作std::atomic對(duì)于簡(jiǎn)單的計(jì)數(shù)器、標(biāo)志位使用互斥量顯得有些“殺雞用牛刀”因?yàn)殒i的獲取和釋放是有開銷的。C11提供了std::atomic模板用于定義原子類型。對(duì)原子類型的操作是不可分割的因此是線程安全的且通常比鎖的性能更高。#include iostream #include thread #include vector #include atomic std::atomicint atomic_counter(0); // 原子計(jì)數(shù)器 void atomic_increment() { for (int i 0; i 100000; i) { atomic_counter; // 原子自增線程安全 // 等價(jià)于 atomic_counter.fetch_add(1, std::memory_order_relaxed); } } int main() { std::thread t1(atomic_increment); std::thread t2(atomic_increment); t1.join(); t2.join(); std::cout Final atomic counter value: atomic_counter std::endl; // 穩(wěn)定輸出200000 return 0; }std::atomic的優(yōu)勢(shì)與局限優(yōu)勢(shì)性能高無(wú)鎖或使用CPU提供的原子指令適用于簡(jiǎn)單的讀寫、加減、交換等操作。局限只能保證單個(gè)變量的操作是原子的。如果邏輯涉及多個(gè)原子變量或一個(gè)原子變量和一個(gè)非原子變量的復(fù)合操作仍然需要額外的同步如鎖來(lái)保證整體一致性。std::atomicbool flag1{false}, flag2{false}; // 線程A flag1.store(true, std::memory_order_relaxed); if (flag2.load(std::memory_order_relaxed)) { /* 操作 */ } // 線程B flag2.store(true, std::memory_order_relaxed); if (flag1.load(std::memory_order_relaxed)) { /* 操作 */ } // 這里兩個(gè)if語(yǔ)句看到的flag1和flag2的順序可能不同可能導(dǎo)致邏輯錯(cuò)誤。需要更強(qiáng)的內(nèi)存序如std::memory_order_acq_rel或鎖。內(nèi)存序Memory Orderstd::atomic操作可以指定內(nèi)存序如std::memory_order_relaxed,std::memory_order_acquire,std::memory_order_release,std::memory_order_acq_rel,std::memory_order_seq_cst。它定義了原子操作周圍非原子內(nèi)存訪問(wèn)的可見性順序。默認(rèn)是std::memory_order_seq_cst順序一致性最嚴(yán)格也最安全。在性能關(guān)鍵路徑上理解并選擇更寬松的內(nèi)存序可以提升性能但這屬于高級(jí)話題極易出錯(cuò)。對(duì)于初學(xué)者使用默認(rèn)值即可。6. 實(shí)戰(zhàn)進(jìn)階構(gòu)建一個(gè)簡(jiǎn)單的線程池理解了線程創(chuàng)建、同步和通信后我們可以嘗試構(gòu)建一個(gè)簡(jiǎn)單的線程池。線程池可以避免頻繁創(chuàng)建和銷毀線程的開銷復(fù)用一組線程來(lái)處理大量的小任務(wù)。下面是一個(gè)極簡(jiǎn)版的固定大小線程池#include iostream #include vector #include queue #include thread #include mutex #include condition_variable #include functional #include future #include stdexcept class SimpleThreadPool { public: explicit SimpleThreadPool(size_t thread_count std::thread::hardware_concurrency()) : stop(false) { if (thread_count 0) thread_count 1; for (size_t i 0; i thread_count; i) { workers.emplace_back([this] { for (;;) { std::functionvoid() task; { std::unique_lockstd::mutex lock(this-queue_mutex); // 等待條件池子停止或有任務(wù)可執(zhí)行 this-condition.wait(lock, [this] { return this-stop || !this-tasks.empty(); }); // 如果池子已停止且任務(wù)隊(duì)列為空則線程結(jié)束 if (this-stop this-tasks.empty()) { return; } // 取出一個(gè)任務(wù) task std::move(this-tasks.front()); this-tasks.pop(); } // 釋放鎖 task(); // 執(zhí)行任務(wù) } }); } } // 提交一個(gè)任務(wù)返回一個(gè)future以便獲取結(jié)果 templateclass F, class... Args auto enqueue(F f, Args... args) - std::futuretypename std::result_ofF(Args...)::type { using return_type typename std::result_ofF(Args...)::type; // 將任務(wù)包裝成一個(gè)packaged_task以便獲取future auto task std::make_sharedstd::packaged_taskreturn_type()( std::bind(std::forwardF(f), std::forwardArgs(args)...) ); std::futurereturn_type res task-get_future(); { std::unique_lockstd::mutex lock(queue_mutex); if (stop) { throw std::runtime_error(enqueue on stopped ThreadPool); } tasks.emplace([task]() { (*task)(); }); // 將任務(wù)包裝成void()放入隊(duì)列 } condition.notify_one(); // 通知一個(gè)工作線程 return res; } ~SimpleThreadPool() { { std::unique_lockstd::mutex lock(queue_mutex); stop true; } condition.notify_all(); // 喚醒所有線程 for (std::thread worker : workers) { worker.join(); } } private: std::vectorstd::thread workers; std::queuestd::functionvoid() tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; // 使用示例 int main() { SimpleThreadPool pool(4); // 創(chuàng)建4個(gè)線程的池子 std::vectorstd::futureint results; // 提交10個(gè)任務(wù) for (int i 0; i 10; i) { results.emplace_back( pool.enqueue([i] { std::cout 任務(wù) i 開始由線程 std::this_thread::get_id() 執(zhí)行。 std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout 任務(wù) i 結(jié)束。 std::endl; return i * i; }) ); } // 獲取結(jié)果 for (auto result : results) { std::cout 任務(wù)結(jié)果: result.get() std::endl; } return 0; // pool析構(gòu)時(shí)會(huì)自動(dòng)等待所有任務(wù)完成并join所有線程 }這個(gè)簡(jiǎn)單線程池的核心機(jī)制構(gòu)造創(chuàng)建指定數(shù)量的工作線程每個(gè)線程都運(yùn)行一個(gè)循環(huán)不斷從任務(wù)隊(duì)列中取任務(wù)執(zhí)行。任務(wù)隊(duì)列一個(gè)std::queuestd::functionvoid()存儲(chǔ)待執(zhí)行的任務(wù)包裝成無(wú)參數(shù)無(wú)返回值的函數(shù)對(duì)象。同步一個(gè)互斥量queue_mutex保護(hù)任務(wù)隊(duì)列一個(gè)條件變量condition用于工作線程的等待和通知。提交任務(wù)enqueue將用戶傳入的可調(diào)用對(duì)象和參數(shù)通過(guò)std::bind和std::packaged_task包裝成一個(gè)可以異步獲取結(jié)果的std::future。將包裝好的任務(wù)函數(shù)推入隊(duì)列。通知一個(gè)等待中的工作線程。工作線程在循環(huán)中等待條件變量。條件為線程池停止或任務(wù)隊(duì)列非空。被喚醒后從隊(duì)列取出任務(wù)并執(zhí)行。如果線程池已停止且隊(duì)列為空則線程退出循環(huán)結(jié)束運(yùn)行。析構(gòu)設(shè)置停止標(biāo)志通知所有線程然后等待join所有線程結(jié)束。這個(gè)實(shí)現(xiàn)是基礎(chǔ)版本存在一些局限性任務(wù)隊(duì)列無(wú)大小限制可能內(nèi)存耗盡。沒有優(yōu)雅關(guān)閉機(jī)制等待所有已提交任務(wù)完成。沒有處理線程中的異常任務(wù)異常會(huì)導(dǎo)致std::future::get()拋出異常。沒有動(dòng)態(tài)調(diào)整線程數(shù)量的能力。但它清晰地展示了如何用std::thread、std::mutex、std::condition_variable和std::future這些C11工具構(gòu)建一個(gè)有用的并發(fā)組件。在實(shí)際項(xiàng)目中可以考慮使用更成熟的庫(kù)如Intel TBB、微軟的PPL或者C17/20中更高級(jí)的并行算法。7. 調(diào)試多線程程序常見陷阱與工具建議多線程Bug如數(shù)據(jù)競(jìng)爭(zhēng)、死鎖往往難以復(fù)現(xiàn)和定位。以下是一些經(jīng)驗(yàn)和工具常見陷阱共享數(shù)據(jù)未保護(hù)這是最普遍的問(wèn)題。任何可能被多個(gè)線程讀寫的數(shù)據(jù)都必須用鎖或原子操作保護(hù)。鎖粒度不當(dāng)過(guò)粗鎖住整個(gè)函數(shù)或過(guò)大范圍嚴(yán)重降低并發(fā)性能。過(guò)細(xì)鎖太多、太頻繁增加鎖開銷和死鎖風(fēng)險(xiǎn)。在持有鎖時(shí)調(diào)用外部代碼外部代碼可能再去獲取其他鎖導(dǎo)致死鎖或者執(zhí)行緩慢操作導(dǎo)致持有鎖時(shí)間過(guò)長(zhǎng)。條件變量的虛假喚醒如前所述必須使用帶謂詞的wait。std::thread對(duì)象生命周期管理不當(dāng)忘記join或detach導(dǎo)致程序崩潰。detach線程訪問(wèn)已銷毀的局部變量。調(diào)試工具與技巧代碼審查與設(shè)計(jì)良好的設(shè)計(jì)是最好的防御。明確哪些數(shù)據(jù)是共享的哪些是線程局部的。盡量減少共享數(shù)據(jù)多用消息傳遞如隊(duì)列而非共享內(nèi)存。使用-fsanitizethreadGCC/Clang這是一個(gè)強(qiáng)大的編譯時(shí)工具可以在運(yùn)行時(shí)檢測(cè)數(shù)據(jù)競(jìng)爭(zhēng)。它是發(fā)現(xiàn)競(jìng)態(tài)條件的神器。g -stdc11 -g -fsanitizethread -pthread your_program.cpp -o your_program使用-fsanitizeaddress檢測(cè)內(nèi)存錯(cuò)誤如use-after-free這在多線程環(huán)境下也常見。Valgrind (Helgrind, DRD)老牌的內(nèi)存和線程錯(cuò)誤檢測(cè)工具。Helgrind和DRD專門用于檢測(cè)線程錯(cuò)誤如數(shù)據(jù)競(jìng)爭(zhēng)、鎖順序問(wèn)題等。但速度較慢。打印日志在關(guān)鍵位置添加帶線程ID的日志輸出可以幫助理解線程的執(zhí)行順序。但注意打印日志本身尤其是到控制臺(tái)是同步操作可能改變程序的時(shí)間特性掩蓋某些競(jìng)態(tài)條件海森堡Bug。靜態(tài)分析工具如Clang Static Analyzer、Cppcheck等可以檢測(cè)出一些潛在的多線程問(wèn)題模式。多線程編程是C中既強(qiáng)大又危險(xiǎn)的特性。std::thread為你提供了強(qiáng)大的武器但能否用好取決于你對(duì)并發(fā)原理的理解和對(duì)細(xì)節(jié)的把握。從簡(jiǎn)單的任務(wù)并行開始嚴(yán)格遵守同步規(guī)則善用RAII管理資源并借助工具進(jìn)行調(diào)試你就能逐步駕馭這門藝術(shù)寫出高效、健壯的并發(fā)程序。記住多線程的第一要義是正確性其次才是性能。在確保正確之前不要過(guò)早優(yōu)化。

相關(guān)新聞

編程用哪個(gè)AI大模型好?實(shí)測(cè)GPT-5.6和Claude的真實(shí)體驗(yàn)

編程用哪個(gè)AI大模型好?實(shí)測(cè)GPT-5.6和Claude的真實(shí)體驗(yàn)

最近被問(wèn)得最多的一個(gè)問(wèn)題:寫代碼到底用哪個(gè)AI模型好?ChatGPT、Claude、Gemini、Grok,每個(gè)都有人吹,每個(gè)都有人踩。我自己折騰了一圈,發(fā)現(xiàn)最大的問(wèn)題不是"哪個(gè)最強(qiáng)",而是"哪個(gè)最適合你的場(chǎng)景…

2026/8/4 15:03:56 閱讀更多
PlanetScale:大規(guī)模并行實(shí)現(xiàn)分片 Postgres 備份,高速且用途廣!

PlanetScale:大規(guī)模并行實(shí)現(xiàn)分片 Postgres 備份,高速且用途廣!

備份生命周期每 12 小時(shí),備份系統(tǒng)需將繁忙數(shù)據(jù)庫(kù)完整狀態(tài)轉(zhuǎn)化為一致加密快照,且不影響生產(chǎn)查詢。PlanetScale 讓 Postgres 和 MySQL 備份創(chuàng)建、調(diào)度等更輕松,內(nèi)部需協(xié)調(diào)云基礎(chǔ)設(shè)施和 DBMS 工具。分片數(shù)據(jù)庫(kù)備份要啟動(dòng)特定節(jié)點(diǎn)、提取數(shù)據(jù)和重放…

2026/8/4 15:03:15 閱讀更多
Casetify 和 OtterBox 手機(jī)殼對(duì)比:防摔實(shí)測(cè)與顏值設(shè)計(jì),哪個(gè)更防摔又更好看?

Casetify 和 OtterBox 手機(jī)殼對(duì)比:防摔實(shí)測(cè)與顏值設(shè)計(jì),哪個(gè)更防摔又更好看?

如果你正在糾結(jié) Casetify 和 OtterBox 的手機(jī)殼,想知道哪一個(gè)防摔更強(qiáng)、哪一個(gè)更好看,那么直接給出答案:兩者在防摔性能上都達(dá)到軍規(guī)級(jí)跌落防護(hù)標(biāo)準(zhǔn),但 Casetify 在同等防護(hù)水平下提供了更豐富的個(gè)性化定制、藝術(shù)家聯(lián)名與潮流設(shè)計(jì)…

2026/8/4 15:03:15 閱讀更多
如何一鍵解鎖Windows遠(yuǎn)程桌面:SuperRDP終極免費(fèi)解決方案

如何一鍵解鎖Windows遠(yuǎn)程桌面:SuperRDP終極免費(fèi)解決方案

如何一鍵解鎖Windows遠(yuǎn)程桌面:SuperRDP終極免費(fèi)解決方案 【免費(fèi)下載鏈接】SuperRDP Super RDPWrap 項(xiàng)目地址: https://gitcode.com/gh_mirrors/su/SuperRDP 對(duì)于眾多Windows家庭版用戶來(lái)說(shuō),無(wú)法使用遠(yuǎn)程桌面功能一直是個(gè)痛點(diǎn)。微軟限制了家庭版的…

2026/8/4 15:03:15 閱讀更多
模型分享還在發(fā)源文件?一個(gè)鏈接解決安全與便捷的兩難

模型分享還在發(fā)源文件?一個(gè)鏈接解決安全與便捷的兩難

外協(xié)加工中,傳遞三維模型進(jìn)行尺寸確認(rèn)或結(jié)構(gòu)評(píng)審,是制造企業(yè)常見的協(xié)作場(chǎng)景。一個(gè)幾百M(fèi)B的STEP文件,通過(guò)郵箱發(fā)送往往因附件過(guò)大被退回。改用網(wǎng)盤傳輸,上傳下載耗時(shí)較長(zhǎng),且發(fā)送方對(duì)源文件發(fā)出后的流轉(zhuǎn)路徑逐漸失去控制…

2026/8/4 15:03:15 閱讀更多
一個(gè)職業(yè)教育觀察者的手記:我在信工的無(wú)人機(jī)產(chǎn)教融合基地,看到了什么

一個(gè)職業(yè)教育觀察者的手記:我在信工的無(wú)人機(jī)產(chǎn)教融合基地,看到了什么

產(chǎn)教深度融合 共建工業(yè)級(jí)特色專業(yè)(客戶供稿) 文 | 職業(yè)教育行業(yè)觀察員 林遠(yuǎn) 前不久去河北出差,順便走訪了一所職業(yè)院校。 說(shuō)實(shí)話,這兩年看過(guò)的校企合作項(xiàng)目不少。企業(yè)掛牌、設(shè)備捐贈(zèng)、專家講座——流程大抵相同,成果也…

2026/8/4 15:03:15 閱讀更多
Java單例模式詳解:實(shí)現(xiàn)方式與最佳實(shí)踐

Java單例模式詳解:實(shí)現(xiàn)方式與最佳實(shí)踐

1. 單例模式的核心價(jià)值與應(yīng)用場(chǎng)景 單例模式作為創(chuàng)建型設(shè)計(jì)模式的代表,在Java開發(fā)中有著不可替代的地位。它的核心價(jià)值在于確保一個(gè)類在任何情況下都只有一個(gè)實(shí)例存在,并提供一個(gè)全局訪問(wèn)點(diǎn)。這種特性在需要嚴(yán)格控制實(shí)例數(shù)量的場(chǎng)景下尤為重要。 在實(shí)際開…

2026/8/4 14:53:15 閱讀更多
清華大學(xué)重磅EST:植物自導(dǎo)電閃蒸焦耳熱600°C/2600°C兩步法!稀土超積累植物秒級(jí)轉(zhuǎn)化為CeO?-石墨烯電催化劑!

清華大學(xué)重磅EST:植物自導(dǎo)電閃蒸焦耳熱600°C/2600°C兩步法!稀土超積累植物秒級(jí)轉(zhuǎn)化為CeO?-石墨烯電催化劑!

通訊作者:鄧兵、劉建國(guó)通訊單位:清華大學(xué)DOI:https://doi.org/10.1021/acs.est.6c00603研究背景稀土元素(REEs)是清潔能源技術(shù)與電子器件不可或缺的核心原料,然而傳統(tǒng)提取方式依賴能耗高、排放大的采礦與強(qiáng)…

2026/8/4 0:01:30 閱讀更多
貴州師范大學(xué)JCIS:混合焓調(diào)控設(shè)計(jì)PtCoNiCuCr高熵合金!ORR半波電位0.89 V/質(zhì)量活性2.4倍Pt/C!

貴州師范大學(xué)JCIS:混合焓調(diào)控設(shè)計(jì)PtCoNiCuCr高熵合金!ORR半波電位0.89 V/質(zhì)量活性2.4倍Pt/C!

研究背景質(zhì)子交換膜燃料電池(PEMFCs)因其高能量轉(zhuǎn)換效率和清潔零排放特性備受關(guān)注,然而陰極氧還原反應(yīng)(ORR)動(dòng)力學(xué)遲緩、鉑催化劑成本高昂且耐久性不足的問(wèn)題嚴(yán)重制約了其商業(yè)化進(jìn)程。將 Pt 與 3d 過(guò)渡金屬合金化可調(diào)控…

2026/8/4 0:01:30 閱讀更多
福州大學(xué)/清華大學(xué)AFM:脈沖焦耳熱900°C/1s合成Co?Cu催化劑,寬電位NH?法拉第效率~100%,MEA穩(wěn)定300h

福州大學(xué)/清華大學(xué)AFM:脈沖焦耳熱900°C/1s合成Co?Cu催化劑,寬電位NH?法拉第效率~100%,MEA穩(wěn)定300h

通訊作者:萬(wàn)宇馳、張久俊、呂瑞濤通訊單位:福州大學(xué) 、清華大學(xué)DOI:https://doi.org/10.1002/adfm.76112核心導(dǎo)讀:本文提出"分步升級(jí)"廢硝酸鹽處理新路線——利用廢水中的金屬離子經(jīng)快速焦耳熱(40V&#xff…

2026/8/4 0:01:30 閱讀更多
MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案

MoneyPrinterPlus實(shí)戰(zhàn)指南:AI視頻批量生成與自動(dòng)化發(fā)布完整解決方案 【免費(fèi)下載鏈接】MoneyPrinterPlus AI一鍵批量生成各類短視頻,自動(dòng)批量混剪短視頻,自動(dòng)把視頻發(fā)布到抖音,快手,小紅書,視頻號(hào)上,賺錢從來(lái)沒有這么容易過(guò)! 支持本地語(yǔ)音模型chatTTS,fasterwhisper,…

2026/8/4 13:11:34 閱讀更多
3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南

3分鐘搞定!QQ空間歷史說(shuō)說(shuō)完整備份終極指南 【免費(fèi)下載鏈接】GetQzonehistory 獲取QQ空間發(fā)布的歷史說(shuō)說(shuō) 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否曾想過(guò),那些年發(fā)過(guò)的QQ空間說(shuō)說(shuō),那些記錄青春的文字…

2026/8/4 13:10:06 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應(yīng)用材料(Applied Materials)公司生產(chǎn)的一款用于半導(dǎo)體設(shè)備的I/O信號(hào)分配電路板。該型號(hào)(0100-02186)的核心特點(diǎn)如下:專用于Endura等半導(dǎo)體工藝腔室。集成信號(hào)路由與分配功能。連接控制…

2026/8/3 19:34:52 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機(jī),適用于自動(dòng)化設(shè)備及通用機(jī)械驅(qū)動(dòng)。該型號(hào)(FFMN-32L-10-T0 40AX)的核心特點(diǎn)如下:三相交流異步電動(dòng)機(jī)。額定…

2026/8/3 19:34:54 閱讀更多