C++中多線程間共享數(shù)據(jù)詳解
在 C++ 中,我們可以通過構造 std::mutex (mutual exclusion)的實例來創(chuàng)建互斥,調(diào)用成員函數(shù) lock() 對其加鎖,調(diào)用 unlock() 解鎖。但是不推薦直接調(diào)用成員函數(shù)的做法,因為這樣做,那我們就必須在該函數(shù)的每條路徑上都調(diào)用 unlock(),包括異常導致退出的路徑。取而代之,C++標準庫提供了類模板 std::lock_guard<>,針對互斥類融合RAII手法:在構造的時候給互斥加鎖,在析構時進行解鎖,從而保證互斥總被正確解鎖。
std::mutex的基本用法
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value){
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
bool list_contains(int value_to_find){
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}
防范死鎖
防范死鎖的通常建議是,始終按相同順序?qū)蓚€互斥加鎖。若我們總是先鎖互斥A,再鎖互斥B,則永遠不會發(fā)生死鎖。但是有時會出現(xiàn)一些棘手的問題,例如,一個函數(shù),其操作同一個類的兩個實例,交換它們內(nèi)部的數(shù)據(jù)為了保證并發(fā)時,免受其他改動的影響,需要對它們進行加鎖,但是函數(shù)入?yún)⒌捻樞蚩梢愿淖?,即可以是swap(A,B)也可以是swap(B,A),當同時發(fā)生這種情況時,死鎖也會發(fā)生。因此需要一種方式來,來同時鎖住兩個互斥,而不是誰先誰后。即要求“全員共同成敗”(all-or-nothing,或全部成功鎖定,或沒獲取任何鎖并拋出異常)的語義。
使用 std::lock() 函數(shù),同時鎖住互斥。
class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X{
private:
some_big_object some_detail;
std::mutex m;
public:
X(const some_big_object &sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs){
if(&lhs == &rhs){
return;
}
std::lock(lhs.m, rhs.m);
std::lock_guard lock_a(lhs.m, std::adopt_lock);
std::lock_guard lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
}
std::lock() 是一個函數(shù),并不像 RAII 類一樣,在析構時進行解鎖。因此需要借助 std::lock_guard 類型的對象,讓 std::mutex 在函數(shù)完成后進行解鎖。std::adopt_lock 作為函數(shù)參數(shù),表示 std::mutex 已經(jīng)被鎖住了,讓std::lock_guard 類不需要在構造函數(shù)中對 std::mutex 進行加鎖。C++ 17出現(xiàn)了 std::scoped_lock<> 模板類,其和 std::lock_guard<>完全等價,只不過前者是可變參數(shù)模板,接受各種互斥型別作為模板參數(shù),還以多個互斥對象作為構造函數(shù)的參數(shù)列表,除了這些其還有與 std::lock() 函數(shù)一樣的功能,因此可以改善上述代碼。
void swap(X& lhs, X& rhs){
if(&lhs == &rhs){
return;
}
std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.some_detail, rhs.some_detail);
}
std::lock_guard 和 std::unique_lock

std::unique_lock 類支持在構造時暫時不獲得鎖,在需要的時候手動調(diào)用 lock(),而獲得鎖。其含有一個內(nèi)部標志 __owns__(可以通過其成員函數(shù) owns_lock() 獲得),表明關聯(lián)的互斥目前是否正被該類的實例上鎖。假如 std::unique_lock 實例關聯(lián)的互斥的確上鎖了,則其析構函數(shù)必須調(diào)用unlock();若不然,實例并未將關聯(lián)的互斥上鎖,便絕不能調(diào)用 unlock()。
初始化時保護共享數(shù)據(jù)
void undefined_behaviour_with_double_checked_locking()
{
if(!resource_ptr){ //①
std::lock_guard<std::mutex> lk(resource_mutex); // ②
if(!resource_ptr){
resource_ptr.reset(new some_resource);
}
}
resource_ptr->do_something();
}
在雙重檢查鎖定模式中,一號線程執(zhí)行到①發(fā)現(xiàn)條件滿足(resource_ptr 為空),同時二號線程執(zhí)行到①也發(fā)現(xiàn)條件滿足,然后繼續(xù)執(zhí)行上鎖操作②,此時一號線程也會去執(zhí)行上鎖操作,但是resource_mutex上的鎖已經(jīng)被二號線程持有了,這樣就會發(fā)生數(shù)據(jù)競爭(當然會發(fā)生惡性數(shù)據(jù)競爭的路徑不止這一條)。
為了解決這種情況,C++標準庫中提供了 std::onec_flag 類 和 std::call_once 類。
std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;
void init_resource()
{
resource_ptr.reset(new some_resource);
}
void foo()
{
std::call_once(resource_flag, init_resource);
resource_ptr->do_something();
}
保護讀多寫少的數(shù)據(jù)
C++ 17 標準庫提供了兩種新的互斥:std::shared_mutex 和 std::shared_timed_mutex(C++14就有了)。兩者的區(qū)別在于后者支持更多操作。
std::shared_mutex mutex; // 獲取讀鎖(又叫共享鎖) std::shared_lock read_lock(mutex); // 獲取寫鎖(又叫排它鎖) std::lock_guard write_lock(mutex);
假設共享鎖已經(jīng)被某些線程持有,若別的線程試圖獲取排他鎖,就會發(fā)生阻塞,直到這些線程全都釋放該共享鎖。反之,如果任一線程持有排他鎖,那么其他線程都無法獲取共享鎖或排它鎖,直到持鎖線程將排它鎖釋放為止。
到此這篇關于C++中多線程間共享數(shù)據(jù)詳解的文章就介紹到這了,更多相關C++共享數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MinGW-w64 C/C++編譯器下載和安裝的方法步驟(入門教程)
如果電腦沒有安裝MinGW-w64 C/C++編譯器,就無法運行gcc命令,本文主要介紹了MinGW-w64 C/C++編譯器下載和安裝的方法步驟,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02

