C++中線程同步與互斥的四種方式介紹及對比詳解
引言
在C++中,當兩個或更多的線程需要訪問共享數(shù)據(jù)時,就會出現(xiàn)線程安全問題。這是因為,如果沒有適當?shù)耐綑C制,一個線程可能在另一個線程還沒有完成對數(shù)據(jù)的修改就開始訪問數(shù)據(jù),這將導致數(shù)據(jù)的不一致性和程序的不可預測性。為了解決這個問題,C++提供了多種線程同步和互斥的機制。
1. 互斥量(Mutex)
互斥量是一種同步機制,用于防止多個線程同時訪問共享資源。在C++中,可以使用std::mutex類來創(chuàng)建互斥量。
#include <thread> #include <mutex> std::mutex mtx; // 全局互斥量 int shared_data = 0; // 共享數(shù)據(jù) void thread_func() { for (int i = 0; i < 10000; ++i) { mtx.lock(); // 獲取互斥量的所有權(quán) ++shared_data; // 修改共享數(shù)據(jù) mtx.unlock(); // 釋放互斥量的所有權(quán) } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 輸出20000 return 0; }
在上述代碼中,我們創(chuàng)建了一個全局互斥量mtx和一個共享數(shù)據(jù)shared_data。然后,我們在thread_func函數(shù)中使用mtx.lock()和mtx.unlock()來保護對shared_data的訪問,確保在任何時候只有一個線程可以修改shared_data。
2. 鎖(Lock)
除了直接使用互斥量,C++還提供了std::lock_guard和std::unique_lock兩種鎖,用于自動管理互斥量的所有權(quán)。
#include <thread> #include <mutex> std::mutex mtx; // 全局互斥量 int shared_data = 0; // 共享數(shù)據(jù) void thread_func() { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 創(chuàng)建鎖,自動獲取互斥量的所有權(quán) ++shared_data; // 修改共享數(shù)據(jù) // 鎖在離開作用域時自動釋放互斥量的所有權(quán) } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 輸出20000 return 0; }
在上述代碼中,我們使用std::lock_guard來自動管理互斥量的所有權(quán)。當創(chuàng)建std::lock_guard對象時,它會自動獲取互斥量的所有權(quán),當std::lock_guard對象離開作用域時,它會自動釋放互斥量的所有權(quán)。這樣,我們就不需要手動調(diào)用mtx.lock()和mtx.unlock(),可以避免因忘記釋放互斥量而導致的死鎖。
3. 條件變量(Condition Variable)
條件變量是一種同步機制,用于在多個線程之間同步條件的變化。在C++中,可以使用std::condition_variable類來創(chuàng)建條件變量。
#include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; // 全局互斥量 std::condition_variable cv; // 全局條件變量 bool ready = false; // 共享條件 void print_id(int id) { std::unique_lock<std::mutex> lock(mtx); // 創(chuàng)建鎖,自動獲取互斥量的所有權(quán) while (!ready) { // 如果條件不滿足 cv.wait(lock); // 等待條件變量的通知 } // 當收到條件變量的通知,且條件滿足時,繼續(xù)執(zhí)行 std::cout << "thread " << id << '\n'; } void go() { std::unique_lock<std::mutex> lock(mtx); // 創(chuàng)建鎖,自動獲取互斥量的所有權(quán) ready = true; // 修改共享條件 cv.notify_all(); // 通知所有等待的線程 } int main() { std::thread threads[10]; for (int i = 0; i < 10; ++i) threads[i] = std::thread(print_id, i); std::cout << "10 threads ready to race...\n"; go(); // 開始比賽 for (auto& th : threads) th.join(); return 0; }
在上述代碼中,我們創(chuàng)建了一個全局互斥量mtx、一個全局條件變量cv和一個共享條件ready。然后,我們在print_id函數(shù)中使用cv.wait(lock)來等待條件變量的通知,當收到條件變量的通知,且條件滿足時,繼續(xù)執(zhí)行。在go函數(shù)中,我們修改共享條件,并使用cv.notify_all()來通知所有等待的線程。
4. 原子操作(Atomic Operation)
原子操作是一種特殊的操作,它可以在多線程環(huán)境中安全地對數(shù)據(jù)進行讀寫,而無需使用互斥量或鎖。在C++中,可以使用std::atomic模板類來創(chuàng)建原子類型。
#include <thread> #include <atomic> std::atomic<int> shared_data(0); // 共享數(shù)據(jù) void thread_func() { for (int i = 0; i < 10000; ++i) { ++shared_data; // 原子操作 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_data << std::endl; // 輸出20000 return 0; }
在上述代碼中,我們創(chuàng)建了一個原子類型的共享數(shù)據(jù)shared_data
。然后,我們在thread_func
函數(shù)中使用++shared_data
來進行原子操作,這樣,我們就不需要使用互斥量或鎖,也可以保證在任何時候只有一個線程可以修改shared_data
。
5. 對比
策略 | 優(yōu)點 | 缺點 |
---|---|---|
單一全局互斥量 | 簡單 | 可能導致嚴重的性能問題,降低并發(fā)性 |
多個互斥量 | 提高并發(fā)性 | 增加程序復雜性,需要避免死鎖 |
原子操作 | 提高并發(fā)性,避免互斥量開銷 | 增加程序復雜性,需要理解和使用原子操作 |
讀寫鎖 | 提高并發(fā)性,特別是讀操作多于寫操作時 | 增加程序復雜性,需要管理讀寫鎖,需要避免死鎖 |
案例舉例
假設(shè)我們正在開發(fā)一個在線聊天 服務(wù)器,需要處理大量的并發(fā)連接。每個連接都有一個關(guān)聯(lián)的用戶對象,用戶對象包含了用戶的狀態(tài)信息,如用戶名、在線狀態(tài)等。
在這種情況下,我們可以使用多個互斥量的策略。我們可以將用戶對象劃分為幾個組,每個組有一個關(guān)聯(lián)的互斥量。當一個線程需要訪問一個用戶對象時,它只需要鎖定該用戶對象所在組的互斥量,而不是所有的用戶對象。這樣,不同的線程可以同時訪問不同的用戶對象,從而提高并發(fā)性。
同時,我們也可以使用讀寫鎖的策略。因為在大多數(shù)情況下,線程只需要讀取用戶的狀態(tài)信息,而不需要修改。所以,我們可以使用讀寫鎖,允許多個線程同時讀取用戶對象,但在修改用戶對象時需要獨占鎖。
在實踐中,我們可能需要結(jié)合使用這兩種策略,以達到最佳的效果。
6. 更進一步:原子操作+鎖
原子操作和鎖是兩種不同的線程同步機制,它們可以單獨使用,也可以一起使用,具體取決于你的應(yīng)用場景。
原子操作是一種低級的同步機制,它可以保證對單個內(nèi)存位置的讀寫操作是原子的,即在任何時候只有一個線程可以對內(nèi)存位置進行操作。原子操作通常用于實現(xiàn)高級的同步機制,如鎖和條件變量。
鎖是一種高級的同步機制,它可以保證對一段代碼或多個內(nèi)存位置的訪問是原子的,即在任何時候只有一個線程可以執(zhí)行被鎖保護的代碼或訪問被鎖保護的內(nèi)存位置。
如果你在使用鎖的同時還使用原子操作,那么你需要確保你的代碼正確地理解和使用這兩種同步機制。例如,如果你在一個被鎖保護的代碼段中使用原子操作,那么你需要確保原子操作不會違反鎖的語義,即在任何時候只有一個線程可以執(zhí)行被鎖保護的代碼。
以下是一個使用原子操作和鎖的例子:
#include <thread> #include <mutex> #include <atomic> std::mutex mtx; // 全局互斥量 std::atomic<int> counter(0); // 原子計數(shù)器 void thread_func() { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 獲取互斥量的所有權(quán) ++counter; // 原子操作 // 鎖在離開作用域時自動釋放互斥量的所有權(quán) } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << counter << std::endl; // 輸出20000 return 0; }
在上述代碼中,我們使用std::lock_guard來獲取互斥量的所有權(quán),然后使用++counter來進行原子操作。這樣,我們既保證了在任何時候只有一個線程可以執(zhí)行被鎖保護的代碼,也保證了對counter的操作是原子的。
總的來說,原子操作和鎖可以一起使用,但你需要確保你的代碼正確地理解和使用這兩種同步機制。
總結(jié)
在C++中,當兩個或更多的線程需要訪問共享數(shù)據(jù)時,可以使用互斥量、鎖、條件變量和原子操作等多種線程同步和互斥的機制來保證線程安全。選擇哪種機制,取決于具體的應(yīng)用場景和需求。
以上就是C++中線程同步與互斥的四種方式介紹及對比詳解的詳細內(nèi)容,更多關(guān)于C++線程同步與互斥方式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一篇文章帶你了解C語言的一些重要字符串與內(nèi)存函數(shù)
這篇文章主要介紹了C語言字符函數(shù)、內(nèi)存函數(shù) 功能,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09C語言順序表的基本操作(初始化,插入,刪除,查詢,擴容,打印,清空等)
這篇文章主要介紹了C語言順序表的基本操作(初始化,插入,刪除,查詢,擴容,打印,清空等),具有很好的參考價值,希望對大家有所幫助。2023-02-02C++將二叉樹轉(zhuǎn)為雙向鏈表及判斷兩個鏈表是否相交
這篇文章主要介紹了C++將二叉樹轉(zhuǎn)為雙向鏈表及判斷兩個鏈表是否相交的方法,文中還給出了求兩個鏈表相交的第一個節(jié)點列的實現(xiàn)方法,需要的朋友可以參考下2016-02-02