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

