C++11鎖機制mutex、lock_guard與unique_lock詳細解析
大家好啊,我是小康。今天咱們聊點"家常"——那些讓C++程序員又愛又恨的多線程同步工具!
如果你曾經(jīng)被多線程搞得頭大,或者聽到"死鎖"就心慌,那這篇文章就是為你準備的。今天我要用最接地氣的方式,幫你徹底搞懂C++11中的三兄弟:mutex
、lock_guard
和unique_lock
。
為啥要用這些同步工具?
先別急著學怎么用,咱們得先知道為啥要用??!
想象一下:你和室友共用一個衛(wèi)生間。如果你們同時沖進去...嗯,畫面太美不敢想象。所以你們會怎么做?肯定是先看看有沒有人,沒人才進去,然后反鎖門,用完了再開門。
多線程程序也一樣!不同的線程可能會同時訪問同一塊"地盤"(共享資源),如果不加控制,就會出現(xiàn)數(shù)據(jù)錯亂、程序崩潰等一系列災難。
這時候,我們的三兄弟就閃亮登場了!
老大:mutex(互斥鎖)
mutex
就像那個衛(wèi)生間的門鎖,它是最基礎的同步工具,核心功能就兩個:鎖上(lock
)和開鎖(unlock
)。
來看個最簡單的例子:
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; // 這就是我們的"門鎖" int shared_value = 0; // 這是我們要保護的"衛(wèi)生間" void increment_value() { mtx.lock(); // 進去之前先鎖門 std::cout << "線程 " << std::this_thread::get_id() << " 進入臨界區(qū)" << std::endl; // 想象這是個很復雜的操作,需要一些時間 shared_value++; std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "線程 " << std::this_thread::get_id() << " 即將離開,共享值為: " << shared_value << std::endl; mtx.unlock(); // 用完了記得開鎖,讓別人能進來 } int main() { std::thread t1(increment_value); std::thread t2(increment_value); t1.join(); t2.join(); return 0; }
看著挺簡單對吧?但這有個大坑——如果在lock
和unlock
之間發(fā)生了異常,或者你單純忘記了unlock
,那么鎖就永遠不會被釋放,其他線程永遠進不了"衛(wèi)生間"!這就是傳說中的"死鎖"。
正因如此,直接使用mutex
很容易出錯,所以C++11給我們提供了更智能的解決方案。
老二:lock_guard(保安大哥)
lock_guard
就像一個靠譜的保安大哥。當你進"衛(wèi)生間"時,他會自動鎖門;當你出來時,無論是正常出來還是因為突發(fā)情況(異常)跑出來,他都會負責解鎖。
看看用lock_guard
如何改寫上面的例子:
void safer_increment() { std::lock_guard<std::mutex> guard(mtx); // 保安上崗,自動鎖門 std::cout << "線程 " << std::this_thread::get_id() << " 進入臨界區(qū)" << std::endl; // 即使這里拋出異常,離開函數(shù)作用域時lock_guard也會自動解鎖 shared_value++; std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "線程 " << std::this_thread::get_id() << " 即將離開,共享值為: " << shared_value << std::endl; // 不需要手動解鎖,guard離開作用域時會自動解鎖 }
是不是簡單多了?這就是RAII(資源獲取即初始化)的魅力——資源的管理跟對象的生命周期綁定在一起。lock_guard
一旦創(chuàng)建就會鎖定互斥量,一旦銷毀(離開作用域)就會解鎖互斥量。
不過lock_guard
有個局限性:一旦上鎖,在其生命周期內(nèi)你就不能手動解鎖了。就像你請了個特別死板的保安,他堅持要等你徹底離開才會開門,中途想出去透個氣都不行。
老三:unique_lock(萬能管家)
如果說lock_guard
是保安大哥,那unique_lock
就是一個高級管家,不但能自動鎖門解鎖,還能根據(jù)你的指令隨時鎖門或開門,甚至可以"借"鑰匙給別人。
來看個例子:
void flexible_operation() { std::unique_lock<std::mutex> superlock(mtx); // 默認情況下構造時會鎖定mutex std::cout << "線程 " << std::this_thread::get_id() << " 開始工作" << std::endl; shared_value++; // 假設這里不需要鎖了,可以提前解鎖 superlock.unlock(); std::cout << "臨時解鎖,執(zhí)行一些不需要保護的操作" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 需要再次訪問共享資源時,可以重新上鎖 superlock.lock(); shared_value++; std::cout << "線程 " << std::this_thread::get_id() << " 完成工作,共享值為: " << shared_value << std::endl; // 同樣,不需要手動解鎖,離開作用域時會自動解鎖(如果當時處于鎖定狀態(tài)) }
除了手動lock
和unlock
,unique_lock
還有更多高級功能:
std::unique_lock<std::mutex> master_lock(mtx, std::defer_lock); // 創(chuàng)建時不鎖定 if (master_lock.try_lock()) { // 嘗試鎖定,如果失敗也不會阻塞 std::cout << "成功獲取鎖!" << std::endl; } else { std::cout << "獲取鎖失敗,但我可以去做別的事" << std::endl; } // 還可以配合條件變量使用 std::condition_variable cv; std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 這里會自動解鎖并等待條件滿足
unique_lock
比lock_guard
靈活,但也付出了一點性能代價,它內(nèi)部需要維護更多狀態(tài)信息。
三兄弟大比拼
說了這么多,來個簡單對比:
特性 | mutex | lock_guard | unique_lock |
---|---|---|---|
手動鎖定/解鎖 | ? | ? | ? |
異常安全 | ?(需手動保證) | ? | ? |
條件變量配合 | ? | ? | ? |
嘗試鎖定(try_lock) | ? | ? | ? |
性能開銷 | 最小 | 很小 | 稍大 |
使用難度 | 容易出錯 | 簡單安全 | 靈活但復雜 |
實戰(zhàn):模擬ATM取款與系統(tǒng)維護
最后用一個貼近生活的例子來鞏固一下。假設我們有個ATM系統(tǒng),既要處理用戶取款,又要處理銀行的系統(tǒng)維護:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> class ATMSystem { private: double cash_available; // ATM中可用現(xiàn)金 bool maintenance_mode; // 是否處于維護模式 std::mutex mtx; std::condition_variable cv; // 條件變量,用于等待維護結束 public: ATMSystem(double initial_cash) : cash_available(initial_cash), maintenance_mode(false) {} // 用戶取款操作 bool withdraw(double amount) { // 這里必須用unique_lock,因為條件變量wait需要它 std::unique_lock<std::mutex> lock(mtx); // 如果ATM正在維護中,等待維護結束 cv.wait(lock, [this] { return !maintenance_mode; }); // 檢查余額并取款 if (cash_available >= amount) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); cash_available -= amount; std::cout << "取出: " << amount << ",ATM剩余現(xiàn)金: " << cash_available << std::endl; return true; } std::cout << "ATM現(xiàn)金不足,取款失??!當前剩余: " << cash_available << std::endl; return false; } // 開始系統(tǒng)維護 void start_maintenance() { std::lock_guard<std::mutex> guard(mtx); maintenance_mode = true; std::cout << "ATM進入維護模式,暫停服務" << std::endl; } // 結束系統(tǒng)維護 void end_maintenance() { { std::lock_guard<std::mutex> guard(mtx); maintenance_mode = false; std::cout << "ATM維護完成,恢復服務" << std::endl; } // 通知所有等待的取款線程 cv.notify_all(); } // 補充現(xiàn)金 void refill_cash(double amount) { std::lock_guard<std::mutex> guard(mtx); cash_available += amount; std::cout << "ATM補充現(xiàn)金: " << amount << ",當前總現(xiàn)金: " << cash_available << std::endl; } }; // 模擬用戶線程 void user_thread(ATMSystem& atm, int user_id) { std::cout << "用戶 " << user_id << " 嘗試取款..." << std::endl; atm.withdraw(100); } // 模擬維護線程 void maintenance_thread(ATMSystem& atm) { std::this_thread::sleep_for(std::chrono::milliseconds(20)); atm.start_maintenance(); // 執(zhí)行維護操作 std::this_thread::sleep_for(std::chrono::milliseconds(300)); atm.refill_cash(500); // 維護結束 atm.end_maintenance(); } int main() { ATMSystem atm(300); // 初始現(xiàn)金300元 // 啟動一個維護線程和多個用戶線程 std::thread maint(maintenance_thread, std::ref(atm)); std::vector<std::thread> users; for (int i = 1; i <= 5; ++i) { users.push_back(std::thread(user_thread, std::ref(atm), i)); } // 等待所有線程結束 maint.join(); for (auto& t : users) { t.join(); } return 0; }
總結
- mutex:最基礎的鎖,需要手動鎖定和解鎖,用不好容易出問題,就像自己管理衛(wèi)生間門鎖。
- lock_guard:簡單安全的自動鎖,構造時鎖定,析構時解鎖,但不能中途操作鎖狀態(tài),就像請了個死板但可靠的保安。
- unique_lock:功能最全面的鎖包裝器,靈活性最高,但有輕微的性能開銷,就像一個萬能的管家。
最佳實踐:
- 簡單場景,優(yōu)先使用
lock_guard
- 需要條件變量或靈活鎖定/解鎖時,使用
unique_lock
- 對性能極度敏感的場景,考慮直接使用
mutex
,但要非常小心
希望這篇文章能讓你對C++11的同步工具有個清晰的認識。多線程不再可怕,熟練掌握這"三兄弟",你就能寫出安全高效的并發(fā)程序啦!
到此這篇關于C++11鎖機制三兄弟大比拼:mutex、lock_guard與unique_lock的文章就介紹到這了,更多相關C++ mutex、lock_guard與unique_lock內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
一文帶你了解C語言中的0長度數(shù)組(可變數(shù)組/柔性數(shù)組)
眾所周知,?GNU/GCC?在標準的?C/C++?基礎上做了有實用性的擴展,?零長度數(shù)組(Arrays?of?Length?Zero)?就是其中一個知名的擴展,本文就來聊聊零長度數(shù)組的相關知識吧2023-03-03MFC串口通信發(fā)送16進制數(shù)據(jù)的方法
這篇文章主要為大家詳細介紹了MFC串口通信發(fā)送16進制數(shù)據(jù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01Cocos2d-x中使用CCScrollView來實現(xiàn)關卡選擇實例
這篇文章主要介紹了Cocos2d-x中使用CCScrollView來實現(xiàn)關卡的選擇實例,本文在代碼中用大量注釋講解了CCScrollView的使用,需要的朋友可以參考下2014-09-09