在 C++ 的并發世界里,管理共享資源就像是在一個繁忙的十字路口指揮交通。如果指揮不當,就會發生混亂甚至致命的“死鎖”。C++ 標準庫提供的各種鎖管理工具,就是我們手中的“交通信號燈”,它們各自擁有獨特的職能,幫助我們編寫出安全、高效且優雅的多線程代碼。
1. std::lock_guard
:忠誠的衛士
std::lock_guard
是最基礎、最可靠的守衛。它就像一個忠誠的士兵,一旦被部署(構造),就會牢牢地守住陣地(鎖定互斥量),直到任務完成(超出作用域)自動卸下職責。它從不偷懶,也從不犯錯,無論程序是正常退出還是因異常而中斷,它都確保鎖被安全釋放。
它的優勢在于簡單而純粹:你只需要告訴它要守護哪個互斥量,剩下的它都會為你自動完成。在你的代碼中,如果只需要在一個作用域內獨占訪問一個資源,lock_guard
永遠是你的首選,因為它沒有多余的開銷,也不給你犯錯的機會。
-
核心特性:
- 自動加鎖與解鎖:遵循 RAII 原則,生命周期與作用域綁定。
- 不可移動、不可拷貝:確保鎖的所有權唯一。
- 無死鎖避免:如果你需要鎖定多個互斥量,必須手動確保所有線程都以相同的順序加鎖,否則可能發生死鎖。
-
適用場景:
- 當你需要在一個函數或一個代碼塊中安全地鎖定一個互斥量時。這是最常見的獨占鎖使用模式。
-
示例:
#include <iostream> #include <thread> #include <mutex> #include <vector>std::mutex mtx; int shared_data = 0;void safe_increment() {// 創建 lock_guard 對象,mtx 在這里被鎖定std::lock_guard<std::mutex> lock(mtx);// 在此作用域內安全訪問共享資源shared_data++;// 當 lock 超出作用域,mtx 會自動解鎖 } // 這里會自動調用 lock.unlock()int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(safe_increment);}for (auto& t : threads) {t.join();}std::cout << "最終 shared_data 的值: " << shared_data << std::endl;return 0; }
2. std::unique_lock
:全能的指揮官
std::unique_lock
是一位能力超群的指揮官。它擁有 lock_guard
的所有優點,但其最大的特點是靈活。它不像 lock_guard
那樣死板,你可以在任何時候手動加鎖、解鎖,甚至決定在創建時延遲加鎖。這種靈活性使得它能應對更復雜的戰術。
unique_lock
的真正力量體現在與 std::condition_variable
的協同作戰中。在等待某個條件時,unique_lock
可以優雅地放下手中的鎖,讓出資源,直到被通知時再迅速重新鎖定。這種合作機制是實現生產者-消費者模型、線程池等高級并發模式的核心。
-
核心特性:
- 靈活的加鎖/解鎖控制:提供
lock()
、unlock()
、try_lock()
等成員函數。 - 可延遲加鎖:通過
std::defer_lock
構造,創建對象時不立即加鎖。 - 可移動:可以作為函數參數或返回值,將鎖的所有權轉移。
- 與條件變量配合:是
std::condition_variable::wait()
函數唯一接受的鎖類型。
- 靈活的加鎖/解鎖控制:提供
-
適用場景:
- 當你需要手動控制鎖的加鎖和解鎖時。
- 當你需要與
std::condition_variable
配合,實現等待/通知機制時。 - 當你需要將鎖的所有權從一個函數轉移到另一個函數時。
-
示例:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable>std::mutex mtx; std::condition_variable cv; bool ready = false;void worker_thread() {// 創建 unique_lock 對象,并鎖定互斥量std::unique_lock<std::mutex> lock(mtx);std::cout << "工作線程正在等待條件..." << std::endl;// 等待條件變為 true,wait() 會原子地釋放鎖并進入休眠cv.wait(lock, []{ return ready; }); // 被喚醒后,wait() 會自動重新鎖定互斥量std::cout << "工作線程被喚醒,開始處理數據。" << std::endl; }void main_thread() {std::this_thread::sleep_for(std::chrono::milliseconds(100));// 創建 unique_lock 并鎖定互斥量std::unique_lock<std::mutex> lock(mtx);ready = true;std::cout << "主線程設置條件,并通知工作線程。" << std::endl;// 必須在通知前釋放鎖,以允許被喚醒的線程獲取它lock.unlock(); cv.notify_one(); }int main() {std::thread t1(worker_thread);std::thread t2(main_thread);t1.join();t2.join();return 0; }
3. std::scoped_lock
:智慧的協調者
std::scoped_lock
是 C++17 引入的,它的主要作用是簡化多互斥量加鎖,并使用內置的死鎖避免算法。你可以把它看作是 std::lock_guard
的多功能升級版。
-
核心特性:
- 同時鎖定一個或多個互斥量。
- 內置死鎖避免算法:它會以原子方式嘗試鎖定所有互斥量,如果失敗則回滾并重試,確保不會因鎖順序不一致而死鎖。
- 無手動控制:和
lock_guard
一樣,不能手動加鎖或解鎖。
-
適用場景:
- 當你需要同時鎖定多個互斥量時,這是最安全、最方便的選擇。它徹底消除了死鎖的風險。
-
示例:
#include <iostream> #include <thread> #include <mutex>std::mutex mtx1; std::mutex mtx2;void transfer_data_safe(int from_id, int to_id) {std::cout << "線程 " << std::this_thread::get_id() << " 正在嘗試數據傳輸..." << std::endl;// 一次性鎖定 mtx1 和 mtx2,使用內置的死鎖避免算法// 無論哪個線程先獲得哪個鎖,都保證不會發生死鎖std::scoped_lock lock(mtx1, mtx2);// 模擬數據傳輸std::this_thread::sleep_for(std::chrono::milliseconds(50));std::cout << "線程 " << std::this_thread::get_id() << " 安全地完成了數據傳輸。" << std::endl; } // lock 超出作用域,mtx1 和 mtx2 自動解鎖int main() {std::thread t1(transfer_data_safe, 1, 2);std::thread t2(transfer_data_safe, 2, 1);t1.join();t2.join();return 0; }
4. std::shared_lock
:高效的圖書館管理員
std::shared_lock
是 C++14 引入的,它與 std::shared_mutex
(也叫讀寫鎖)配合使用。它的主要作用是管理共享鎖的所有權,提供一種允許多個線程同時讀取,但只允許一個線程寫入的同步機制。
-
核心特性:
- 共享鎖模式:允許多個線程同時持有鎖,用于并發讀取。
- RAII 風格:在構造時加鎖,在析構時自動解鎖。
- 必須與
std::shared_mutex
配合使用。
-
適用場景:
- 讀多寫少的場景。當讀取數據的頻率遠高于寫入數據時,使用
shared_lock
可以顯著提高程序的并發性能。
- 讀多寫少的場景。當讀取數據的頻率遠高于寫入數據時,使用
-
示例:
#include <iostream> #include <thread> #include <shared_mutex> #include <vector> #include <string>std::string shared_data = "initial"; std::shared_mutex shared_mtx;// 讀者線程:只讀取數據,可以并發執行 void reader_thread(int id) {// 構造 shared_lock 時,請求共享鎖std::shared_lock<std::shared_mutex> lock(shared_mtx);std::cout << "讀者 " << id << " 正在讀取: " << shared_data << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(20)); } // lock 超出作用域,自動釋放共享鎖// 寫者線程:修改數據,獨占訪問 void writer_thread(const std::string& new_data) {// 構造 unique_lock 時,請求獨占鎖,會阻塞所有讀者和寫者std::unique_lock<std::shared_mutex> lock(shared_mtx);std::cout << "寫者正在寫入..." << std::endl;shared_data = new_data;std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // lock 超出作用域,自動釋放獨占鎖int main() {std::vector<std::thread> readers;for (int i = 0; i < 5; ++i) {readers.emplace_back(reader_thread, i);}std::thread writer1(writer_thread, "new data");for (int i = 5; i < 10; ++i) {readers.emplace_back(reader_thread, i);}writer1.join();for (auto& t : readers) {t.join();}std::cout << "最終數據是: " << shared_data << std::endl;return 0; }
5. std::lock
:傳統的鎖定大師
std::lock
是一個函數,而不是一個類。它的主要作用是一次性鎖定多個互斥量,并使用內置的死鎖避免算法。
-
核心特性:
- 函數:不是 RAII 類,通常需要與
std::unique_lock
和std::defer_lock
配合使用。 - 死鎖避免:通過其內部的算法,確保無論傳入互斥量的順序如何,都能安全地加鎖。
- 函數:不是 RAII 類,通常需要與
-
適用場景:
- 在 C++11/14 版本中,是處理多鎖死鎖問題的標準方法。在 C++17 及以后,通常被
std::scoped_lock
所取代。
- 在 C++11/14 版本中,是處理多鎖死鎖問題的標準方法。在 C++17 及以后,通常被
-
示例:
#include <iostream> #include <thread> #include <mutex>std::mutex mtxA; std::mutex mtxB;void process_data_lock_function() {std::cout << "線程 " << std::this_thread::get_id() << " 正在處理數據..." << std::endl;// 延遲加鎖,創建 unique_lock 對象但不立即鎖定互斥量std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);// 使用 std::lock 函數一次性鎖定兩個互斥量std::lock(lockA, lockB);std::cout << "線程 " << std::this_thread::get_id() << " 已安全地獲取所有鎖。" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));// lockA 和 lockB 超出作用域時會自動解鎖 }int main() {std::thread t1(process_data_lock_function);std::thread t2(process_data_lock_function);t1.join();t2.join();return 0; }
總結對比
特性 | std::lock_guard | std::unique_lock | std::scoped_lock | std::shared_lock |
---|---|---|---|---|
鎖定類型 | 獨占鎖 | 獨占鎖 | 獨占鎖 | 共享鎖 |
加鎖數量 | 1 | 1 | 1或多個 | 1 |
靈活性 | 最低 | 最高 | 高 | 中等 |
死鎖避免 | 無 | 無 | 內置 | 無 |
與條件變量 | 否 | 是 | 否 | 是 |
主要用途 | 簡單、安全的單鎖管理 | 需要靈活控制鎖或與條件變量配合 | 安全的多鎖管理 | 讀多寫少場景 |