C++11中互斥鎖的使用
我們現(xiàn)在有一個(gè)需求,我們需要對(duì) g_exceptions
這個(gè) vector 的訪問(wèn)進(jìn)行同步處理,確保同一時(shí)刻只有一個(gè)線程能向它插入新的元素。為此我使用了一個(gè) mutex 和一個(gè)鎖(lock)。mutex 是同步操作的主體,在 C++ 11 的 <mutex>
頭文件中,有四種風(fēng)格的實(shí)現(xiàn):
- mutex:提供了核心的
lock()
unlock()
方法,以及當(dāng) mutex 不可用時(shí)就會(huì)返回的非阻塞方法try_lock()
- recursive_mutex:允許同一線程內(nèi)對(duì)同一 mutex 的多重持有
- timed_mutex: 與
mutex
類(lèi)似,但多了try_lock_for()
try_lock_until()
兩個(gè)方法,用于在特定時(shí)長(zhǎng)里持有 mutex,或持有 mutex 直到某個(gè)特定時(shí)間點(diǎn) - recursive_timed_mutex:
recursive_mutex
和timed_mutex
的結(jié)合
下面是一個(gè)使用 std::mutex
的例子(注意 get_id()
和 sleep_for()
兩個(gè)輔助方法的使用,上文已有提及)。
#include <iostream> #include <thread> #include <mutex> #include <chrono> std::mutex g_lock; void func() { g_lock.lock(); std::cout << "entered thread " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(rand() % 10)); std::cout << "leaving thread " << std::this_thread::get_id() << std::endl; g_lock.unlock(); } int main() { srand((unsigned int)time(0)); std::thread t1(func); std::thread t2(func); std::thread t3(func); t1.join(); t2.join(); t3.join(); return 0; }
輸出如下:
entered thread 10144
leaving thread 10144
entered thread 4188
leaving thread 4188
entered thread 3424
leaving thread 3424
lock()
unlock()
兩個(gè)方法應(yīng)該很好懂,前者鎖住 mutex,如果該 mutex 不可用,則阻塞線程;稍后,后者解鎖線程。
下面一個(gè)例子展示了一個(gè)簡(jiǎn)單的線程安全的容器(內(nèi)部使用了 std::vector
)。該容器提供用于添加單一元素的 add()
方法,以及添加多個(gè)元素的 addrange()
方法(內(nèi)部調(diào)用 add()
實(shí)現(xiàn))。
注意:盡管如此,下面會(huì)指出,由于 va_args
的使用等原因,這個(gè)容器并非真正線程安全。此外,dump()
方法不應(yīng)屬于容器,在實(shí)際實(shí)現(xiàn)中它應(yīng)該作為一個(gè)獨(dú)立的輔助函數(shù)。這個(gè)例子的目的僅僅是展示 mutex 的相關(guān)概念,而非實(shí)現(xiàn)一個(gè)完整的線程安全的容器。
template <typename T> class container { std::mutex _lock; std::vector<T> _elements; public: void add(T element) { _lock.lock(); _elements.push_back(element); _lock.unlock(); } void addrange(int num, ...) { va_list arguments; va_start(arguments, num); for (int i = 0; i < num; i++) { _lock.lock(); add(va_arg(arguments, T)); _lock.unlock(); } va_end(arguments); } void dump() { _lock.lock(); for(auto e : _elements) std::cout << e << std::endl; _lock.unlock(); } }; void func(container<int>& cont) { cont.addrange(3, rand(), rand(), rand()); } int main() { srand((unsigned int)time(0)); container<int> cont; std::thread t1(func, std::ref(cont)); std::thread t2(func, std::ref(cont)); std::thread t3(func, std::ref(cont)); t1.join(); t2.join(); t3.join(); cont.dump(); return 0; }
當(dāng)你運(yùn)行這個(gè)程序時(shí),會(huì)進(jìn)入死鎖。原因:在 mutex 被釋放前,容器嘗試多次持有它,這顯然不可能。這就是為什么引入 std::recursive_mutex
,它允許一個(gè)線程對(duì) mutex 多重持有。允許的最大持有次數(shù)并不確定,但當(dāng)達(dá)到上限時(shí),線程鎖會(huì)拋出 std::system_error 錯(cuò)誤。因此,要解決上面例子的錯(cuò)誤,除了修改 addrange
令其不再調(diào)用 lock
和 unlock
之外,可以用 std::recursive_mutex
代替 mutex
。
template <typename T> class container { std::recursive_mutex _lock; // ... };
成功輸出:
6334
18467
41
6334
18467
41
6334
18467
41
敏銳的讀者可能注意到,每次調(diào)用 func()
輸出的都是相同的數(shù)字。這是因?yàn)?,seed 是線程局部量,調(diào)用 srand()
只會(huì)在主線程中初始化 seed,在其他工作線程中 seed 并未被初始化,所以每次得到的數(shù)字都是一樣的。
手動(dòng)加鎖和解鎖可能造成問(wèn)題,比如忘記解鎖或鎖的次序出錯(cuò),都會(huì)造成死鎖。C++ 11 標(biāo)準(zhǔn)提供了若干類(lèi)和函數(shù)來(lái)解決這個(gè)問(wèn)題。封裝類(lèi)允許以 RAII 風(fēng)格使用 mutex,在一個(gè)鎖的生存周期內(nèi)自動(dòng)加鎖和解鎖。這些封裝類(lèi)包括:
- lock_guard:當(dāng)一個(gè)實(shí)例被創(chuàng)建時(shí),會(huì)嘗試持有 mutex (通過(guò)調(diào)用
lock()
);當(dāng)實(shí)例銷(xiāo)毀時(shí),自動(dòng)釋放 mutex (通過(guò)調(diào)用unlock()
)。不允許拷貝。- unique_lock:通用 mutex 封裝類(lèi),與
lock_guard
不同,還支持延遲鎖、計(jì)時(shí)鎖、遞歸鎖、移交鎖的持有權(quán),以及使用條件變量。不允許拷貝,但允許轉(zhuǎn)移(move)。
借助這些封裝類(lèi),可以把容器改寫(xiě)為:
template <typename T> class container { std::recursive_mutex _lock; std::vector<T> _elements; public: void add(T element) { std::lock_guard<std::recursive_mutex> locker(_lock); _elements.push_back(element); } void addrange(int num, ...) { va_list arguments; va_start(arguments, num); for (int i = 0; i < num; i++) { std::lock_guard<std::recursive_mutex> locker(_lock); add(va_arg(arguments, T)); } va_end(arguments); } void dump() { std::lock_guard<std::recursive_mutex> locker(_lock); for(auto e : _elements) std::cout << e << std::endl; } }
讀者可能會(huì)提出, dump()
方法不更改容器的狀態(tài),應(yīng)該設(shè)為 const。但如果你添加 const 關(guān)鍵字,會(huì)得到如下編譯錯(cuò)誤:
‘std::lock_guard<_Mutex>::lock_guard(_Mutex &)' : cannot convert parameter 1 from ‘const std::recursive_mutex' to ‘std::recursive_mutex &'
一個(gè) mutex (不管何種風(fēng)格)必須被持有和釋放,這意味著 lock()
unlock
方法必被調(diào)用,這兩個(gè)方法是 non-const 的。所以,邏輯上 lock_guard
的聲明不能是 const (若該方法 為 const,則 mutex 也為 const)。這個(gè)問(wèn)題的解決辦法是,將 mutex 設(shè)為 mutable
。mutable
允許由 const 方法更改 mutex 狀態(tài)。不過(guò),這種用法僅限于隱式的,或「元(meta)」?fàn)顟B(tài)——譬如,運(yùn)算過(guò)的高速緩存、檢索完成的數(shù)據(jù),使得下次調(diào)用能瞬間完成;或者,改變像 mutex 之類(lèi)的位元,僅僅作為一個(gè)對(duì)象的實(shí)際狀態(tài)的補(bǔ)充。
template <typename T> class container { mutable std::recursive_mutex _lock; std::vector<T> _elements; public: void dump() const { std::lock_guard<std::recursive_mutex> locker(_lock); for(auto e : _elements) std::cout << e << std::endl; } };
這些封裝類(lèi)鎖的構(gòu)造函數(shù)可以通過(guò)重載的聲明來(lái)指定鎖的策略??捎玫牟呗杂校?/p>
defer_lock_t
類(lèi)型的defer_lock
:不持有 mutextry_to_lock_t
類(lèi)型的try_to_lock
: 嘗試持有 mutex 而不阻塞線程adopt_lock_t
類(lèi)型的adopt_lock
:假定調(diào)用它的線程已持有 mutex
這些策略的聲明方式如下:
struct defer_lock_t { }; struct try_to_lock_t { }; struct adopt_lock_t { }; constexpr std::defer_lock_t defer_lock = std::defer_lock_t(); constexpr std::try_to_lock_t try_to_lock = std::try_to_lock_t(); constexpr std::adopt_lock_t adopt_lock = std::adopt_lock_t();
除了這些 mutex 封裝類(lèi)之外,標(biāo)準(zhǔn)庫(kù)還提供了兩個(gè)方法用于鎖住一個(gè)或多個(gè) mutex:
- lock:鎖住 mutex,通過(guò)一個(gè)避免了死鎖的算法(通過(guò)調(diào)用
lock()
,try_lock()
和unlock()
實(shí)現(xiàn)) - try_lock:嘗試通過(guò)調(diào)用
try_lock()
來(lái)調(diào)用多個(gè) mutex,調(diào)用次序由 mutex 的指定次序而定
下面是一個(gè)死鎖案例:有一個(gè)元素容器,以及一個(gè) exchange()
函數(shù)用于互換兩個(gè)容器里的某個(gè)元素。為了實(shí)現(xiàn)線程安全,這個(gè)函數(shù)通過(guò)一個(gè)和容器關(guān)聯(lián)的 mutex,對(duì)這兩個(gè)容器的訪問(wèn)進(jìn)行同步。
template <typename T> class container { public: std::mutex _lock; std::set<T> _elements; void add(T element) { _elements.insert(element); } void remove(T element) { _elements.erase(element); } }; void exchange(container<int>& cont1, container<int>& cont2, int value) { cont1._lock.lock(); std::this_thread::sleep_for(std::chrono::seconds(1)); // <-- forces context switch to simulate the deadlock cont2._lock.lock(); cont1.remove(value); cont2.add(value); cont1._lock.unlock(); cont2._lock.unlock(); }
假如這個(gè)函數(shù)在兩個(gè)線程中被調(diào)用,在其中一個(gè)線程中,一個(gè)元素被移出容器 1 而加到容器 2;在另一個(gè)線程中,它被移出容器 2 而加到容器 1。這可能導(dǎo)致死鎖——當(dāng)一個(gè)線程剛持有第一個(gè)鎖,程序馬上切入另一個(gè)線程的時(shí)候。
int main() { srand((unsigned int)time(NULL)); container<int> cont1; cont1.add(1); cont1.add(2); cont1.add(3); container<int> cont2; cont2.add(4); cont2.add(5); cont2.add(6); std::thread t1(exchange, std::ref(cont1), std::ref(cont2), 3); std::thread t2(exchange, std::ref(cont2), std::ref(cont1), 6); t1.join(); t2.join(); return 0; }
要解決這個(gè)問(wèn)題,可以使用 std::lock
,保證所有的鎖都以不會(huì)死鎖的方式被持有:
void exchange(container<int>& cont1, container<int>& cont2, int value) { std::lock(cont1._lock, cont2._lock); cont1.remove(value); cont2.add(value); cont1._lock.unlock(); cont2._lock.unlock(); }
總結(jié)
- 創(chuàng)建一個(gè)mutex對(duì)象:使用std::mutex創(chuàng)建一個(gè)互斥鎖。
- 加鎖操作:在進(jìn)入臨界區(qū)之前調(diào)用lock()方法,以獲取獨(dú)占式訪問(wèn)權(quán)限。
- 解鎖操作:在退出臨界區(qū)時(shí)調(diào)用unlock()方法釋放持有的獨(dú)占式訪問(wèn)權(quán)限。
- 使用RAII進(jìn)行自動(dòng)加解鎖管理:可以通過(guò)定義 std::unique_lock/std::shared_lock/ std::scoped_lock 來(lái)簡(jiǎn)化加解鎖過(guò)程并避免手工管理死鎖等風(fēng)險(xiǎn)。
- 防止死鎖問(wèn)題:如果需要同時(shí)獲得多個(gè)互斥器上的所有權(quán),請(qǐng)確保按照相同順序獲取它們,否則可能會(huì)發(fā)生死鎖。另外,應(yīng)盡量減小臨界區(qū)大小以提高性能,并考慮使用其他同步原語(yǔ)如條件變量、信號(hào)量等來(lái)實(shí)現(xiàn)更復(fù)雜的同步需求。
- 盡可能地避免使用全局變量: 在多線程編程環(huán)境中, 全局變量很容易導(dǎo)致競(jìng)態(tài)條件(race condition),因此我們應(yīng)該盡可能地將共享數(shù)據(jù)限制到某些具體的作用域,如對(duì)象內(nèi)部等。
- 小心使用遞歸鎖:std::recursive_mutex允許同一個(gè)線程多次獲得鎖,并在最后一次解除鎖定。但是,在實(shí)際應(yīng)用中,這種機(jī)制可能會(huì)導(dǎo)致死鎖問(wèn)題和性能瓶頸等問(wèn)題,因此必須謹(jǐn)慎地使用。
到此這篇關(guān)于C++11中互斥鎖的使用的文章就介紹到這了,更多相關(guān)C++11 互斥鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用C++11實(shí)現(xiàn)Android系統(tǒng)的Handler機(jī)制
這篇文章主要介紹了使用C++11實(shí)現(xiàn)Android系統(tǒng)的Handler機(jī)制,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04深入探討:main函數(shù)執(zhí)行完畢后,是否可能會(huì)再執(zhí)行一段代碼?
本篇文章是對(duì)main函數(shù)執(zhí)行完畢后,是否可能會(huì)再執(zhí)行一段代碼,進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05詳解VS2010實(shí)現(xiàn)創(chuàng)建并生成動(dòng)態(tài)鏈接庫(kù)dll的方法
在某些應(yīng)用程序場(chǎng)景下,需要將一些類(lèi)或者方法編譯成動(dòng)態(tài)鏈接庫(kù)dll,以便別的.exe或者.dll文件可以通過(guò)第三方庫(kù)的方式進(jìn)行調(diào)用,下面就簡(jiǎn)單介紹一下如何通過(guò)VS2010來(lái)創(chuàng)建動(dòng)態(tài)鏈接庫(kù)2022-12-12C語(yǔ)言實(shí)現(xiàn)個(gè)人財(cái)務(wù)管理
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)個(gè)人財(cái)務(wù)管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11淺談哈希表存儲(chǔ)效率一般不超過(guò)50%的原因
下面小編就為大家?guī)?lái)一篇淺談哈希表存儲(chǔ)效率一般不超過(guò)50%的原因。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01C語(yǔ)言中if語(yǔ)句加大括號(hào)和不加大括號(hào)的區(qū)別介紹
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言中if語(yǔ)句加大括號(hào)和不加大括號(hào)的區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12C++實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)框架SimpleNN的詳細(xì)過(guò)程
本來(lái)自己想到用C++實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)主要是想強(qiáng)化一下編碼能力并入門(mén)深度學(xué)習(xí),對(duì)C++實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)框架SimpleNN的詳細(xì)過(guò)程感興趣的朋友一起看看吧2021-08-08