淺析C++中的線程同步機(jī)制的實(shí)現(xiàn)那
1. 為什么需要線程同步?
當(dāng)多個線程并發(fā)訪問共享數(shù)據(jù)(內(nèi)存、文件、網(wǎng)絡(luò)連接等)時,如果不進(jìn)行任何同步控制,可能會引發(fā)一系列問題,最典型的是:
- 數(shù)據(jù)競爭:一個線程在讀數(shù)據(jù)時,另一個線程在寫數(shù)據(jù),導(dǎo)致讀到的數(shù)據(jù)是“臟的”、不完整的或邏輯錯誤的。
- 破壞不變量:對象在修改過程中,其內(nèi)部狀態(tài)可能暫時是不一致的(例如,修改一個鏈表時)。如果另一個線程在此時訪問該對象,會看到這個破碎的狀態(tài),導(dǎo)致未定義行為。
線程同步的核心目的是:通過強(qiáng)制特定代碼段的互斥訪問或執(zhí)行順序,來保證多線程環(huán)境下程序行為的正確性和可預(yù)測性。
2. C++標(biāo)準(zhǔn)庫提供的同步機(jī)制
C++11在標(biāo)準(zhǔn)庫中引入了 <thread> 和 <mutex> 等頭文件,提供了豐富的同步原語。
2.1 互斥量 - 保證互斥訪問
互斥量是最基礎(chǔ)的同步工具,它確保同一時間只有一個線程可以進(jìn)入被保護(hù)的代碼段(臨界區(qū))。
a) std::mutex 最基本的互斥量,不可遞歸。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex g_mutex;
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
g_mutex.lock(); // 加鎖
++shared_data; // 臨界區(qū)
g_mutex.unlock(); // 解鎖
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << shared_data << std::endl; // 一定是 200000
return 0;
}
注意:直接使用 lock() 和 unlock() 是危險的,如果臨界區(qū)代碼拋出異常,可能導(dǎo)致互斥量無法解鎖,引發(fā)死鎖。永遠(yuǎn)優(yōu)先使用RAII包裝器。
b) std::lock_guard 最簡單的RAII包裝器,在構(gòu)造時加鎖,析構(gòu)時自動解鎖。
void safe_increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(g_mutex); // 構(gòu)造時加鎖
++shared_data; // 臨界區(qū)
} // 作用域結(jié)束,lock析構(gòu),自動解鎖
}
c) std::unique_lock 比 lock_guard 更靈活,但開銷稍大。它允許延遲加鎖、提前解鎖、條件變量配合使用等。
void flexible_increment() {
for (int i = 0; i < 100000; ++i) {
std::unique_lock<std::mutex> lock(g_mutex, std::defer_lock); // 延遲加鎖
// ... 一些不涉及共享數(shù)據(jù)的操作 ...
lock.lock(); // 手動加鎖
++shared_data;
lock.unlock(); // 可以手動提前解鎖
// ... 其他操作 ...
}
}
d) std::recursive_mutex 允許同一個線程多次獲取同一個互斥量而不會死鎖。用于可能遞歸調(diào)用或需要多次加鎖的場景。應(yīng)謹(jǐn)慎使用,通常表明設(shè)計(jì)可能有問題。
2.2 條件變量 - 線程間的通信與等待
條件變量允許線程阻塞等待某個條件成立,或在條件成立時通知其他線程。它必須與互斥量配合使用。
std::condition_variable(推薦,通常更高效)std::condition_variable_any(可與任何滿足基本互斥量概念的類型一起使用,但開銷更大)
典型生產(chǎn)者-消費(fèi)者模型:
#include <queue>
#include <condition_variable>
std::queue<int> g_queue;
std::mutex g_mutex;
std::condition_variable g_cv;
bool g_done = false;
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(g_mutex);
g_queue.push(i);
std::cout << "Produced: " << i << std::endl;
}
g_cv.notify_one(); // 通知一個等待的消費(fèi)者
}
{
std::lock_guard<std::mutex> lock(g_mutex);
g_done = true;
}
g_cv.notify_all(); // 通知所有消費(fèi)者結(jié)束
}
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(g_mutex);
// 等待條件:隊(duì)列不為空或生產(chǎn)結(jié)束
g_cv.wait(lock, [] { return !g_queue.empty() || g_done; });
// 被喚醒后,需要重新檢查條件
if (g_done && g_queue.empty()) {
break;
}
// 消費(fèi)數(shù)據(jù)
int data = g_queue.front();
g_queue.pop();
lock.unlock(); // 盡早釋放鎖
std::cout << "Consumer " << id << " consumed: " << data << std::endl;
}
}
關(guān)鍵點(diǎn):
wait操作會原子地釋放互斥鎖并使線程休眠。- 被喚醒時,它會重新獲取互斥鎖,然后檢查條件(使用提供的謂詞)。必須使用循環(huán)或帶謂詞的wait來防止“虛假喚醒”。
2.3 信號量 - C++20
信號量是一個更底層的同步原語,它維護(hù)一個計(jì)數(shù)器,用于控制對特定數(shù)量資源的訪問。
std::counting_semaphore:允許至少LeastMaxValue個并發(fā)訪問。std::binary_semaphore:是std::counting_semaphore<1>的別名,類似于互斥量,但可由不同線程進(jìn)行鎖和解鎖。
#include <semaphore>
std::binary_semaphore smph(0); // 初始值為0
void waiter() {
std::cout << "Waiting...\n";
smph.acquire(); // 等待信號量值>0,然后減1
std::cout << "Finished waiting!\n";
}
void notifier() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Notifying...\n";
smph.release(); // 信號量值加1,喚醒等待者
}
2.4 鎖存器和屏障 - C++20
用于管理一組線程的同步點(diǎn)。
std::latch:一次性使用的倒計(jì)時門閂。線程在arrive_and_wait上阻塞,直到內(nèi)部計(jì)數(shù)器減為0,所有阻塞線程被同時釋放。不可重復(fù)使用。std::barrier:可重復(fù)使用的同步機(jī)制。它允許一組線程執(zhí)行一系列階段。在每個階段,線程到達(dá)屏障并阻塞,直到所有線程都到達(dá),然后所有線程被釋放,屏障進(jìn)入下一個階段。
3. 高級話題與底層原理
3.1 死鎖與預(yù)防
死鎖通常發(fā)生在兩個或以上線程互相等待對方持有的資源時。
產(chǎn)生條件(四個必要條件):
- 互斥訪問
- 持有并等待
- 不可剝奪
- 循環(huán)等待
預(yù)防策略:
- 固定順序上鎖:所有線程都按照相同的全局順序獲取鎖。
- 使用 std::lock 或 std::scoped_lock (C++17):一次性鎖定多個互斥量,避免死鎖。
std::mutex mutex1, mutex2; void safe_lock() { // std::lock 使用死鎖避免算法(如Dijkstra算法)來同時鎖定多個互斥量 std::lock(mutex1, mutex2); // 使用 std::adopt_lock 表示互斥量已被鎖定,lock_guard只需接管所有權(quán) std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock); // ... } // C++17 更簡潔的方式: void safer_lock() { std::scoped_lock lock(mutex1, mutex2); // 自動使用死鎖避免算法 // ... } - 避免嵌套鎖:如果可能,盡量只持有一個鎖。
- 使用層次鎖:為鎖分配層級編號,只允許以編號遞減的順序獲取鎖。
3.2 性能考量
- 鎖的粒度:鎖保護(hù)的臨界區(qū)應(yīng)盡可能小。在臨界區(qū)內(nèi)不要進(jìn)行耗時操作(如I/O)。
- 鎖競爭:當(dāng)多個線程頻繁嘗試獲取同一個鎖時,會發(fā)生激烈競爭,導(dǎo)致大量線程在用戶態(tài)和內(nèi)核態(tài)之間切換,嚴(yán)重降低性能。
- 解決方案:使用無鎖數(shù)據(jù)結(jié)構(gòu)、減少共享數(shù)據(jù)、使用讀寫鎖(
std::shared_mutex)、或者將數(shù)據(jù)分區(qū)(每個線程處理自己的數(shù)據(jù)副本,最后再合并)。
- 解決方案:使用無鎖數(shù)據(jù)結(jié)構(gòu)、減少共享數(shù)據(jù)、使用讀寫鎖(
3.3 內(nèi)存模型與原子操作
同步機(jī)制的底層與C++內(nèi)存模型緊密相關(guān)。
- std::atomic:提供了無需互斥鎖的線程安全訪問。對于基本數(shù)據(jù)類型(如 int, bool, pointer),使用 std::atomic 通常比 mutex 效率更高,因?yàn)樗苯釉贑PU指令級別保證操作的原子性。
std::atomic<int> atomic_counter(0); void atomic_increment() { for (int i = 0; i < 100000; ++i) { atomic_counter.fetch_add(1, std::memory_order_relaxed); } } - 內(nèi)存序:
std::memory_order允許你控制原子操作周圍的非原子內(nèi)存訪問的可見性順序。這是為了在保證正確性的前提下,追求極致的性能。memory_order_seq_cst(順序一致性):最強(qiáng)保證,默認(rèn)選項(xiàng),性能開銷最大。memory_order_acquire/memory_order_release/memory_order_acq_rel:用于實(shí)現(xiàn)“同步于”關(guān)系。memory_order_relaxed:只保證原子性,不提供同步和順序保證。
除非你是專家,否則請使用 std::atomic 的默認(rèn)內(nèi)存序(memory_order_seq_cst)。
4. 總結(jié)與最佳實(shí)踐
- 優(yōu)先使用RAII:始終使用 std::lock_guard, std::unique_lock, std::scoped_lock,避免手動 lock/unlock。
- 用互斥量保護(hù)數(shù)據(jù),而非代碼:清晰地知道哪些數(shù)據(jù)是共享的,并用最小的鎖粒度來保護(hù)它。
- 慎用遞歸鎖:遞歸鎖通常意味著糟糕的設(shè)計(jì)。
- 使用條件變量進(jìn)行事件等待:不要使用忙等待(while (!condition) {}),這會浪費(fèi)CPU資源。
- 警惕死鎖:使用鎖順序、std::lock 等策略來預(yù)防。
- 性能瓶頸在于鎖競爭:優(yōu)化方向是減少共享和縮小臨界區(qū),而非盲目追求“無鎖”。無鎖編程極其復(fù)雜且容易出錯。
- 簡單場景用 atomic,復(fù)雜同步用 mutex:對于簡單的計(jì)數(shù)器或標(biāo)志位,std::atomic 是更好的選擇。對于復(fù)雜的對象或需要等待條件的情況,使用 mutex 和 condition_variable。
- 理解工具適用場景:
mutex:互斥訪問。condition_variable:等待條件成立。semaphore:控制資源池訪問。latch/barrier:多線程分階段協(xié)同。
通過深入理解這些同步機(jī)制的原理、代價和適用場景,你才能寫出既正確又高效的多線程C++程序。
到此這篇關(guān)于淺析C++中的線程同步機(jī)制的實(shí)現(xiàn)那的文章就介紹到這了,更多相關(guān)C++ 線程同步機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C/C++ Linux出錯處理函數(shù)(strerror與perror)的使用
我們知道,系統(tǒng)函數(shù)調(diào)用不能保證每次都成功,必須進(jìn)行出錯處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。本文主要為大家介紹兩個出錯處理函數(shù)(strerror、perror)的使用,需要的可以參考一下2023-01-01
C語言程序設(shè)計(jì)譚浩強(qiáng)第五版課后答案(第三章習(xí)題答案)
這篇文章主要介紹了C語言程序設(shè)計(jì)譚浩強(qiáng)第五版課后答案(第三章習(xí)題答案),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-04-04
基于對話框程序中讓對話框捕獲WM_KEYDOWN消息的實(shí)現(xiàn)方法
下面我們將通過程序給大家演示基于對話框的應(yīng)用程序?qū)M_KEYDOWN消息的捕獲。需要的朋友可以參考下2013-05-05
C/C++編程判斷String字符串是否包含某個字符串實(shí)現(xiàn)示例
這篇文章主要為大家介紹了C++編程中判斷String字符串是否包含某個字符串的實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11
C++實(shí)現(xiàn)藍(lán)橋杯競賽題目---搭積木
這篇文章主要介紹了C++實(shí)現(xiàn)藍(lán)橋杯競賽題目---搭積木,本篇文章通過題目分析列舉公式進(jìn)行分析算法,包含詳細(xì)的圖文,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07

