C++實現(xiàn)多線程并發(fā)場景下的同步方法
如果在多線程程序中對全局變量的訪問沒有進行適當?shù)耐娇刂疲ɡ缡褂没コ怄i、原子變量等),會導致多個線程同時訪問和修改全局變量時發(fā)生競態(tài)條件(race condition)。這種競態(tài)條件可能會導致一系列不確定和嚴重的后果。
在C++中,可以通過使用互斥鎖(mutex)、原子操作、讀寫鎖來實現(xiàn)對全局變量的互斥訪問。
一、缺乏同步控制造成的后果
1. 數(shù)據(jù)競爭(Data Race)
數(shù)據(jù)競爭發(fā)生在多個線程同時訪問同一個變量,并且至少有一個線程在寫該變量時沒有進行同步。由于缺少同步機制,多個線程對全局變量的操作可能會相互干擾,導致變量的值不可預測。
示例:
#include <iostream> #include <thread> int globalVar = 0; void increment() { globalVar++; } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Global variable: " << globalVar << std::endl; return 0; }
后果:
- 上面的代碼中,
globalVar++
并不是一個原子操作。它由多個步驟組成:讀取值、增加值、寫回。在這段代碼中,t1
和t2
可能會同時讀取globalVar
的值,導致兩個線程同時修改它的值,最終的結果會小于預期的2
。這就是典型的數(shù)據(jù)競爭。
2. 不一致的狀態(tài)(Inconsistent State)
在沒有同步控制的情況下,多個線程可能會對全局變量進行同時讀寫操作,導致變量處于不一致的狀態(tài)。例如,多個線程可能會同時讀取和修改相同的變量,導致最終狀態(tài)不符合預期。
示例: 假設你有一個程序要求維護一個全局的計數(shù)器。如果沒有加鎖來確保線程安全,兩個線程同時執(zhí)行時,計數(shù)器可能會被寫成一個無意義的值。
#include <iostream> #include <thread> int counter = 0; void increment() { for (int i = 0; i < 100000; ++i) { counter++; // 非線程安全操作 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter: " << counter << std::endl; return 0; }
后果:
- 在沒有同步的情況下,
counter++
可能會導致多個線程在同一時刻讀取到相同的計數(shù)器值,并同時將相同的更新值寫回變量,這會使得counter
的最終值遠小于預期的200000
。 - 這可能會導致程序的業(yè)務邏輯錯誤,特別是如果全局變量用作關鍵狀態(tài)的標識。
3. 崩潰或程序未定義行為
由于數(shù)據(jù)競爭或者不一致的狀態(tài),程序可能會進入一個不可預測的狀態(tài),導致崩潰。全局變量的值在多線程的競爭中可能會發(fā)生損壞,從而導致未定義的行為(undefined behavior)。
例如:
- 訪問已釋放內存:一個線程修改了全局變量并釋放了相關內存,但其他線程仍然試圖訪問該內存。
- 內存覆蓋:多個線程同時修改全局變量,導致不同線程的操作互相覆蓋,從而引發(fā)崩潰。
二、互斥鎖std::mutex實現(xiàn)同步
std::mutex
是C++標準庫中的一種機制,用于避免多個線程同時訪問同一個資源(如全局變量)時發(fā)生競爭條件。
下面是一個示例,展示了如何使用std::mutex
來保護全局變量:
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; // 定義全局互斥鎖 int globalVar = 0; // 定義全局變量 void threadFunction() { std::lock_guard<std::mutex> lock(mtx); // 上鎖,確保互斥 // 訪問和修改全局變量 ++globalVar; std::cout << "Global variable: " << globalVar << std::endl; // 鎖會在lock_guard離開作用域時自動釋放 } int main() { std::thread t1(threadFunction); std::thread t2(threadFunction); t1.join(); t2.join(); return 0; }
說明:
std::mutex
: 用于保護共享資源(如全局變量)。std::lock_guard<std::mutex>
: 是一個RAII風格的封裝器,它在構造時自動上鎖,在析構時自動解鎖,確保了線程安全。threadFunction
中,每個線程在訪問globalVar
之前都會先獲得互斥鎖,這樣就能確保線程之間不會同時訪問和修改全局變量。
使用std::mutex
可以防止不同線程之間因競爭訪問全局變量而引發(fā)的錯誤或不一致問題。
有時如果你需要更細粒度的控制,還可以考慮使用std::unique_lock
,它比std::lock_guard
更靈活,允許手動控制鎖的獲取和釋放。
三、獨占鎖std::unique_lock實現(xiàn)同步
std::unique_lock
是 C++11 標準庫中的一種互斥鎖包裝器,它提供了比 std::lock_guard
更靈活的鎖管理方式。std::unique_lock
允許手動控制鎖的獲取和釋放,而不僅僅是在對象生命周期結束時自動釋放鎖(如 std::lock_guard
所做的那樣)。這使得它比 std::lock_guard
更加靈活,適用于更復雜的場景,比如需要在同一作用域內多次鎖定或解鎖,或者需要在鎖定期間進行一些其他操作。
std::unique_lock 的關鍵特性:
- 手動控制鎖的獲取和釋放:
std::unique_lock
支持手動解鎖和重新鎖定,它比std::lock_guard
更加靈活。 - 延遲鎖定和提前解鎖:你可以選擇在對象創(chuàng)建時延遲鎖定,或者在鎖定后手動釋放鎖。
- 支持條件變量:
std::unique_lock
支持與條件變量一起使用,這是std::lock_guard
無法做到的。
基本用法:
1. 構造時自動加鎖:
std::unique_lock
默認會在構造時自動加鎖。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; void threadFunction() { std::unique_lock<std::mutex> lock(mtx); // 構造時自動上鎖 std::cout << "Thread is running\n"; // 臨界區(qū)的操作 // 鎖會在 lock 對象超出作用域時自動釋放 } int main() { std::thread t1(threadFunction); std::thread t2(threadFunction); t1.join(); t2.join(); return 0; }
2. 手動解鎖與重新加鎖:
std::unique_lock
允許你在鎖定期間手動解鎖和重新加鎖,這對于一些需要臨時釋放鎖的場景非常有用。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; void threadFunction() { std::unique_lock<std::mutex> lock(mtx); // 構造時自動上鎖 std::cout << "Thread is running\n"; // 臨界區(qū)的操作 lock.unlock(); // 手動解鎖 std::cout << "Lock released temporarily\n"; // 臨界區(qū)之外的操作 lock.lock(); // 重新加鎖 std::cout << "Lock acquired again\n"; // 臨界區(qū)操作繼續(xù)進行 } int main() { std::thread t1(threadFunction); std::thread t2(threadFunction); t1.join(); t2.join(); return 0; }
3. 延遲鎖定:
std::unique_lock
也允許你延遲鎖定,通過傳遞一個 std::defer_lock
參數(shù)給構造函數(shù)來實現(xiàn)。這會創(chuàng)建一個未鎖定的 std::unique_lock
,你可以在稍后手動調用 lock()
來加鎖。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; void threadFunction() { std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延遲加鎖 std::cout << "Thread is preparing to run\n"; // 做一些不需要加鎖的操作 lock.lock(); // 手動加鎖 std::cout << "Thread is running under lock\n"; // 臨界區(qū)的操作 } int main() { std::thread t1(threadFunction); std::thread t2(threadFunction); t1.join(); t2.join(); return 0; }
4. 條件變量:
std::unique_lock
是與條件變量一起使用的理想選擇,它支持對互斥鎖的手動解鎖和重新加鎖。這在條件變量的使用場景中非常有用,因為在等待條件時需要解鎖互斥鎖,而在條件滿足時重新加鎖。
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void threadFunction() { std::unique_lock<std::mutex> lock(mtx); // 上鎖 while (!ready) { // 等待 ready 為 true cv.wait(lock); // 等待,自動解鎖并掛起線程 } std::cout << "Thread is running\n"; } void notify() { std::this_thread::sleep_for(std::chrono::seconds(1)); // 模擬一些操作 std::cout << "Notifying the threads\n"; std::unique_lock<std::mutex> lock(mtx); // 上鎖 ready = true; cv.notify_all(); // 通知所有線程 } int main() { std::thread t1(threadFunction); std::thread t2(threadFunction); std::thread notifier(notify); t1.join(); t2.join(); notifier.join(); return 0; }
解釋:
std::condition_variable
和std::unique_lock
:- 在
threadFunction
中,cv.wait(lock)
會釋放鎖并等待條件變量的通知。 std::unique_lock
能夠在調用wait
時自動釋放鎖,并且在wait
返回時會重新加鎖,這使得std::unique_lock
成為使用條件變量的最佳選擇。
- 在
cv.notify_all()
:通知所有等待該條件的線程,thread1
和thread2
都會在條件滿足時繼續(xù)執(zhí)行。
四、共享鎖std::shared_mutex實現(xiàn)同步
std::shared_mutex
是 C++17 引入的一個同步原語,它提供了一種讀寫鎖機制,允許多個線程共享讀取同一資源,而只有一個線程能夠獨占寫入該資源。相比于傳統(tǒng)的 std::mutex
(只支持獨占鎖),std::shared_mutex
可以提高并發(fā)性,特別是在讀操作遠多于寫操作的情況下。
std::shared_mutex 的工作原理:
- 共享鎖(shared lock):多個線程可以同時獲取共享鎖,這意味著多個線程可以同時讀取共享資源。多個線程獲取共享鎖時不會發(fā)生沖突。
- 獨占鎖(unique lock):只有一個線程可以獲取獨占鎖,這意味著寫操作會阻塞其他所有操作(無論是讀操作還是寫操作),以保證數(shù)據(jù)的一致性。
使用 std::shared_mutex:
std::shared_mutex
提供了兩種類型的鎖:
std::unique_lock<std::shared_mutex>
:用于獲取獨占鎖。std::shared_lock<std::shared_mutex>
:用于獲取共享鎖。
1. 基本使用示例:
#include <iostream> #include <thread> #include <shared_mutex> #include <vector> std::shared_mutex mtx; // 定義一個 shared_mutex int sharedData = 0; void readData(int threadId) { std::shared_lock<std::shared_mutex> lock(mtx); // 獲取共享鎖 std::cout << "Thread " << threadId << " is reading data: " << sharedData << std::endl; } void writeData(int threadId, int value) { std::unique_lock<std::shared_mutex> lock(mtx); // 獲取獨占鎖 sharedData = value; std::cout << "Thread " << threadId << " is writing data: " << sharedData << std::endl; } int main() { std::vector<std::thread> threads; // 啟動多個線程進行讀取操作 for (int i = 0; i < 5; ++i) { threads.push_back(std::thread(readData, i)); } // 啟動一個線程進行寫入操作 threads.push_back(std::thread(writeData, 100, 42)); // 等待所有線程結束 for (auto& t : threads) { t.join(); } return 0; }
解釋:
- 共享鎖 (
std::shared_lock
):線程readData
使用std::shared_lock
獲取共享鎖,這允許多個線程同時讀取sharedData
,因為讀取操作是線程安全的。 - 獨占鎖 (
std::unique_lock
):線程writeData
使用std::unique_lock
獲取獨占鎖,這確保了只有一個線程可以寫sharedData
,并且寫操作會阻塞所有其他線程(包括讀操作和寫操作)。
2. 多個讀線程與單個寫線程的并發(fā)控制:
在這個示例中,多個讀線程可以并行執(zhí)行,因為它們都獲取了共享鎖。只有當寫線程(獲取獨占鎖)執(zhí)行時,其他線程(無論是讀線程還是寫線程)會被阻塞。
- 寫操作:獲取獨占鎖,所有讀操作和寫操作都會被阻塞,直到寫操作完成。
- 讀操作:多個線程可以同時獲取共享鎖,只有在沒有寫操作時才會執(zhí)行。
3. 共享鎖與獨占鎖的沖突:
- 共享鎖:多個線程可以同時獲取共享鎖,只要沒有線程持有獨占鎖。共享鎖不會阻塞其他共享鎖請求。
- 獨占鎖:當一個線程持有獨占鎖時,其他任何線程的共享鎖或獨占鎖請求都會被阻塞,直到獨占鎖釋放。
4. 使用場景:
std::shared_mutex
主要適用于讀多寫少的場景。假設有一個資源(如緩存、數(shù)據(jù)結構),它在大部分時間內被多個線程讀取,但偶爾需要被更新。在這種情況下,std::shared_mutex
可以讓多個讀操作并行執(zhí)行,同時避免寫操作導致的不必要的阻塞。
例如:
- 緩存數(shù)據(jù)讀取:多個線程可以并發(fā)讀取緩存中的數(shù)據(jù),而當緩存需要更新時,獨占鎖會確保數(shù)據(jù)一致性。
- 數(shù)據(jù)庫的并發(fā)查詢和修改:多個線程可以并發(fā)查詢數(shù)據(jù)庫,但只有一個線程可以執(zhí)行寫操作。
5. std::shared_mutex 與 std::mutex 比較:
std::mutex
:提供獨占鎖,適用于寫操作頻繁且不需要并發(fā)讀的場景。每次加鎖時,其他線程都無法進入臨界區(qū)。std::shared_mutex
:適用于讀多寫少的場景,允許多個線程同時讀取共享資源,但寫操作會阻塞所有其他操作。
6. 性能考慮:
- 讀操作頻繁時:使用
std::shared_mutex
可以提高并發(fā)性,因為多個線程可以同時讀取數(shù)據(jù)。 - 寫操作頻繁時:性能可能會低于
std::mutex
,因為寫操作需要獨占資源并阻塞所有其他操作。
7. 條件變量:
與 std::mutex
一樣,std::shared_mutex
也可以與條件變量(std::condition_variable
)一起使用,不過在使用時要注意,不同的線程需要加鎖和解鎖對應的鎖。
#include <iostream> #include <thread> #include <shared_mutex> #include <condition_variable> std::shared_mutex mtx; std::condition_variable_any cv; int sharedData = 0; void readData() { std::shared_lock<std::shared_mutex> lock(mtx); // 獲取共享鎖 while (sharedData == 0) { // 等待數(shù)據(jù)可用 cv.wait(lock); // 等待數(shù)據(jù)被寫入 } std::cout << "Reading data: " << sharedData << std::endl; } void writeData(int value) { std::unique_lock<std::shared_mutex> lock(mtx); // 獲取獨占鎖 sharedData = value; std::cout << "Writing data: " << sharedData << std::endl; cv.notify_all(); // 通知所有等待的線程 } int main() { std::thread reader(readData); std::thread writer(writeData, 42); reader.join(); writer.join(); return 0; }
解釋:
std::shared_lock
:用于共享讀鎖,允許多個線程同時讀取。cv.wait(lock)
:使用共享鎖來等待某些條件的變化。cv.notify_all()
:通知所有等待線程,喚醒它們繼續(xù)執(zhí)行。
五、std::atomic實現(xiàn)同步
std::atomic
是 C++11 標準引入的一種類型,用于實現(xiàn)原子操作。原子操作指的是操作在執(zhí)行過程中不可被中斷,因此能夠保證數(shù)據(jù)的一致性和正確性。
std::atomic
提供了一些基本的原子操作方法,這些操作是不可分割的,保證了在多線程環(huán)境下線程安全。它主要用于數(shù)據(jù)的同步與協(xié)作,避免了傳統(tǒng)同步原語(如鎖、條件變量)所帶來的性能瓶頸。
原子操作的基本概念:
- 原子性:在執(zhí)行時,操作不能被打斷,保證線程之間對共享變量的操作不會產(chǎn)生競態(tài)條件。
- 內存順序(Memory Ordering):控制操作的執(zhí)行順序和對共享數(shù)據(jù)的可見性,
std::atomic
允許通過內存順序來顯式指定不同線程間的同步行為。
std::atomic 提供的原子操作:
- 加載(Load):從原子變量中讀取數(shù)據(jù)。
- 存儲(Store):將數(shù)據(jù)存儲到原子變量中。
std::atomic 支持的內存順序(Memory Ordering):
std::memory_order_acquire
:確保前面的操作在加載之后執(zhí)行,即它會阻止后續(xù)的操作在此之前執(zhí)行。std::memory_order_release
:確保后面的操作在存儲之前執(zhí)行,即它會阻止前面的操作在此之后執(zhí)行。
通常情況下,在使用 std::atomic
進行同步時,使用 memory_order_release
在 store
操作時,使用 memory_order_acquire
在 load
操作時,是一種常見的模式,特別是在生產(chǎn)者-消費者模式或者其他類似的同步模式下。
memory_order_release
和 memory_order_acquire
一般搭配使用。
這種組合是為了確保 內存順序的一致性,并且保證數(shù)據(jù)正確的可見性。具體來說:
memory_order_release
:在執(zhí)行store
操作時,它會確保在store
之前的所有操作(如數(shù)據(jù)寫入)不會被重排序到store
之后,保證當前線程的寫操作對其他線程是可見的。因此,store
操作保證所有前置的寫操作都會在這個store
完成后被其他線程看到。memory_order_acquire
:在執(zhí)行load
操作時,它會確保在load
之后的所有操作(如數(shù)據(jù)讀?。┎粫恢嘏判虻?nbsp;load
之前,保證當前線程在讀取共享數(shù)據(jù)后,后續(xù)的操作可以看到正確的數(shù)據(jù)。在load
之前的所有操作(包括對共享變量的寫入)會在讀取這個值之后對當前線程可見。
這兩者配合使用,確保了線程間的同步,避免了數(shù)據(jù)競態(tài)條件。
具體場景
考慮一個生產(chǎn)者-消費者模型,生產(chǎn)者負責寫入數(shù)據(jù)并通知消費者,消費者負責讀取數(shù)據(jù)并處理。
示例:
#include <iostream> #include <atomic> #include <thread> std::atomic<int> data(0); std::atomic<bool> ready(false); void consumer() { while (!ready.load(std::memory_order_acquire)) { // 等待 ready 為 true } std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl; } void producer() { data.store(42, std::memory_order_relaxed); // 寫數(shù)據(jù) ready.store(true, std::memory_order_release); // 設置 ready 為 true } int main() { std::thread t1(consumer); std::thread t2(producer); t1.join(); t2.join(); return 0; }
解釋:
ready.store(true, std::memory_order_release)
:生產(chǎn)者線程在寫入ready
時使用memory_order_release
,這意味著在ready
設置為true
之后,所有在此之前的操作(如對data
的寫入)對消費者線程是可見的。ready.load(std::memory_order_acquire)
:消費者線程在讀取ready
時使用memory_order_acquire
,這意味著消費者線程在讀取ready
后,確保它能夠看到生產(chǎn)者線程在store
ready
之前所做的所有修改(如data
的值)。
這種組合保證了生產(chǎn)者線程的寫操作(例如 data.store(42)
)對于消費者線程是可見的,且在讀取 ready
后,消費者線程可以安全地讀取到更新后的 data
。
到此這篇關于C++實現(xiàn)多線程并發(fā)場景下的同步方法的文章就介紹到這了,更多相關C++ 多線程并發(fā)同步內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C++構造函數(shù)+復制構造函數(shù)+重載等號運算符調用
這篇文章主要介紹了C++構造函數(shù)+復制構造函數(shù)+重載等號運算符調用,文章敘述詳細,具有一定的的參考價值,需要的小伙伴可以參考一下2022-03-03C語言一看就懂的選擇與循環(huán)語句及函數(shù)介紹
函數(shù)是一個功能模塊,它把實現(xiàn)某個功能的代碼塊包含起來,并起一個函數(shù)名,供別人調用,如printf函數(shù),如system函數(shù)。是程序運行當中包裝起來的一個步驟;選擇與循環(huán)是編程中最常用的結構,本篇文章用最簡單的文字帶你了解它們2022-04-04