關(guān)于線程同步與Mutex的使用
一、什么是線程同步?
- 線程同步(Thread Synchronization)是多線程編程中的一個重要概念,它指的是通過一定的機制來控制多個線程之間的執(zhí)行順序,以確保它們能夠正確地訪問和修改共享資源,從而避免數(shù)據(jù)競爭和不一致性問題。
- 在多線程環(huán)境中,多個線程可能同時訪問和修改共享資源(如變量,數(shù)據(jù)結(jié)構(gòu)或文件等)。如果沒有適當?shù)耐綑C制,這些線程可能會以不可預測的順序執(zhí)行,導致數(shù)據(jù)競爭、臟讀、臟寫或其他不可預期的行為。線程同步的目標就是確保線程之間的有序執(zhí)行,以維護數(shù)據(jù)的一致性和完整性。
為什么要同步呢?
如果在同一時刻,僅只有一個線程訪問某個變量(臨界資源),不會存在臟數(shù)據(jù)的問題,但是,如果同一時刻有多個線程,同時訪問同一個臨界資源的時候,就會存在 問題,如何來解決這個問題呢?這就提出了“線程同步”這個概念。
以下是一個簡單的例子:

那么我們有沒有什么辦法,控制這些線程對臨界資源的訪問呢?想個什么辦法才能使它們之間不亂套呢?這就該線程同步機制登場了
二、線程同步機制
C++中提供了多種線程同步機制,常用的方法包括:
- 互斥鎖(Mutex):互斥鎖是最常用的線程同步機制之一,當一個線程想要訪問共享資源時,它首先會嘗試獲取與該資源關(guān)聯(lián)的互斥鎖如果鎖已經(jīng)被其他線程持有,則該線程將被阻塞,直到鎖被釋放。這樣可以確保在任何時候只有一個線程能夠訪問共享資源。
- 條件變量(Condition Variable):條件變量用于使線程在滿足某個條件之前等待,它通常與互斥鎖一起使用,以便在等待條件成立時釋放鎖,并在條件成立時重新獲取鎖。這允許線程在等待期間不占用鎖,從而提高并發(fā)性能。
- 信號量(Semaphore):信號量是一種通用的線程同步機制,它允許多個線程同時訪問共享資源,但限制同時訪問的線程數(shù)量。信號量內(nèi)部維護一個計數(shù)器,用于表示可用資源的數(shù)量。當線程需要訪問資源時,它會嘗試減少計數(shù)器的值;當線程釋放資源時,它會增加計數(shù)器的值。當計數(shù)器的值小于零時,嘗試獲取資源的線程將被阻塞。
- 原子操作(Atomic Operation):原子操作時不可中斷的操作,即在執(zhí)行過程中不會被其他線程打斷。C++11及以后的版本提供了頭文件,其中包含了一系列原子操作的函數(shù)和類。這些原子操作可以用于安全地更新共享數(shù)據(jù),而無需是同互斥鎖等同步機制。
三、互斥鎖(Mutex)
互斥(Mutex)是一種同步機制,同于保護共享資源,放置多個線程同時訪問和修改同一資源,從而引起數(shù)據(jù)競爭(data race)和不一致性。
當一個線程想要訪問某個共享資源時,它首先會嘗試獲取與該資源關(guān)聯(lián)的互斥鎖(mutex)。如果互斥鎖已經(jīng)被其他線程持有(即被鎖定),則該線程將被阻塞,直到互斥鎖被釋放(即被解鎖)。一旦線程成功獲取到互斥鎖,它就可以安全地訪問共享資源,并在訪問完成后釋放互斥鎖,以便其他線程可以獲取該鎖并訪問資源。
互斥鎖通常具有以下幾個特性:
- 互斥性:任意時刻只有一個線程可以持有某個互斥鎖。
- 原子性:對互斥鎖的獲取和釋放操作時原子的,即在執(zhí)行這些操作時不會被其他線程打斷。
- 可重入性:某些互斥鎖類型(如遞歸鎖)允許同一線程多次獲取同一個鎖,但通常不建議這樣做,因為它會增加死鎖的風險。
- 非阻塞性:雖然互斥鎖本身是一種阻塞性同步機制,但某些高級實現(xiàn)(如嘗試鎖)允許線程在無法立即獲取鎖時繼續(xù)執(zhí)行其他任務(wù),而不是被阻塞。
#include <iostream>
#include <thread>
using namespace std;
//共享變量,沒有互斥鎖或原子操作
int counter = 0;
//線程函數(shù) 對counter進行自增操作
void increment_counter(int times)
{
for(int i=0;i<times;++i)
{
//這是一個數(shù)據(jù)競爭,因為多個線程可能同時執(zhí)行這行代碼
counter++;
}
}
int main()
{
//創(chuàng)建兩個線程,每個線程對counter自增100000次
thread t1(increment_counter,100000);
thread t2(increment_counter,100000);
//等待兩個線程完成
t1.join();
t2.join();
//輸出結(jié)果,這個結(jié)果可能不是200000
cout<<"最終結(jié)果:"<<counter<<endl;
return 0;
}在以上的這個例子中,輸出的結(jié)果可能不是我們想要的那個答案,這是因為我們創(chuàng)建了兩個線程,這兩個線程的同時占用counter這個資源,前一個線程剛修改counter的值,后一個線程可能就會立即覆蓋掉當前這個counter的值。
接下來我們對這個例子進行改造:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//共享變量,沒有互斥鎖或原子操作
int counter = 0;
//定義一個互斥鎖
mutex mymutex;
//線程函數(shù) 對counter進行自增操作
void increment_counter(int times)
{
for(int i=0;i<times;++i)
{
mymutex.lock();//在訪問臨界資源之前先加鎖
counter++;
mymutex.unlock();//訪問完了之后解鎖,把鎖釋放
}
}
int main()
{
//創(chuàng)建兩個線程,每個線程對counter自增100000次
thread t1(increment_counter,100000);
thread t2(increment_counter,100000);
//等待兩個線程完成
t1.join();
t2.join();
//輸出結(jié)果
cout<<"最終結(jié)果:"<<counter<<endl;
return 0;
}通過加上互斥鎖之后,我們輸出的結(jié)果將是正確的。
四、loock 和 unlock
在C++中,使用std::mutex的lock()和unlock()函數(shù)來管理對共享資源的訪問,從而確保在多線程環(huán)境中資源的同步訪問。
以下是關(guān)于如何使用它們以及需要注意的事項:
1.如何使用?
首先,你需要創(chuàng)建一個std::mutex對象
std::mutex mymutex;
在訪問共享資源之前,使用lock()函數(shù)鎖定互斥量
mymutex.lock(); //訪問共享變量 ...............
在互斥鎖被鎖定期間,你可以安全的訪問共享資源,因為其他試圖鎖定該互斥量的線程將被阻塞。
一旦完成對共享資源的訪問,將會使用unlock()函數(shù)解鎖互斥量。
//完成對共享資源的訪問 mymutex.unlock();
2.注意事項
(1)死鎖:
如果線程在持有互斥量的情況下調(diào)用了一個阻塞操作(如另一個互斥量的lock()),并且這個阻塞操作永遠不會完成(因為其他線程持有它需要的資源),那么就會發(fā)生死鎖,避免死鎖的一種方法就是始終按照相同的順序鎖定互斥量,或者使用更高級的同步原語,如std::lock_guard或std::unique_lock,它們可以自動管理鎖的獲取和釋放。
(2)異常安全:
如果在鎖定互斥量之后拋出異常,那么必須確定互斥量被正確解鎖,使用std::lock_guard或std::unique_lock可以自動處理這種情況,因為它們在析構(gòu)時會釋放鎖。
如下面這個例子:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//全局互斥量
mutex mymutex;
void safe_function()
{
std::lock_guard<mutex> lock(mymutex);//鎖定互斥量
//在這里執(zhí)行需要互斥訪問的代碼
//如果拋出異常,lock_guard會在析構(gòu)時自動解鎖mymutex
try{
//模擬一些可能拋出的異常
if(/*some condition that might cause an exception*/){
throw std::runtime_error("An error occurred");//使用拋出異常
}
//......其他代碼塊
}
catch(const std::exception& e){
//處理異常,但不需要擔心解鎖,因為lock_guard會自動處理
std::cerr << "Caught exception:"<<e.what() << '\n';
}
//lock_guard離開作用域時自動解鎖mymutex
}
int main()
{
//假設(shè)這里有一些線程調(diào)用safe_function()
//由于使用了lock_guard,所以wu'lun是否拋出異常,mymutex都會被正確解鎖
//........
return 0;
}(3)不要手動解鎖未鎖定的互斥量:
在調(diào)用unlock()之前,必須確保互斥量已經(jīng)被lock()鎖定,否則,該行為是未定義的。
(4)不要多次鎖定同一互斥量:
對于非遞歸互斥量(如std::mutex),不要再同一線程中多次鎖定它。這會導致未定義的行為。如果需要遞歸鎖定,請使用std::recursive_mutex。
(5)使用RAII管理鎖
使用RAII(資源獲取即初始化)原則來管理鎖的生命周期,通過std::lock_guardhuostd::unique_lock來確保鎖在不需要時自動釋放。
(6)避免長時間持有鎖
盡量縮短持有鎖的時間,以減少線程之間的爭用,提高程序的并發(fā)性能。
(7)考慮使用更高級的同步原語
除了std::mutex之外,C++標準庫還提供了其他更高級的同步原語,如條件變量(std::condition_variable)、讀寫鎖(std::shared_mutex)等,它們可以在特定場景下提供更高效的同步機制。
五、Mutex的四種類型
在C++中,特別是從C++11開始,std::mutex及其相關(guān)類型提供了一系列用于同步多線程訪問共享資源的機制,以下時std::mutex的四種主要類型及其詳細解釋:
1.std::mutex
- 這是最基本的互斥量類型
- 它不允許遞歸鎖頂,即同一個線程不能被多次鎖定同一個std::mutex,如果嘗試這樣做,程序的行為將是未定義的,通常會導致死鎖。
- 它提供了基本的鎖定(lock)和解鎖(unlock)操作
- 當一個線程鎖定了一個std::mutex時,任何其他嘗試鎖定該互斥量的線程都將被阻塞,直到原始線程調(diào)用unlock()釋放它。
2.std::recursive_mutex
- 這是一個遞歸(或可重入)互斥量
- 與std::mutex不同,它允許同一線程多次鎖定一個互斥量。這可以用于需要遞歸訪問受保護資源的場景。
- 線程在每次鎖定時都需要對應(yīng)的解鎖,以確保正確的同步。
- 如果線程沒有正確匹配其鎖定和解鎖操作(即解鎖次數(shù)少于鎖定次數(shù)),則其他線程仍然會被阻塞。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
std::recursive_mutex mtx;
void recursive_function()
{
mtx.lock();//第一次鎖定
cout<<"Thread "<<this_thread::get_id()<<" locked mutex"<<endl;
//遞歸鎖定
mtx.lock();//同一線程可以多次鎖定
cout<<"Thread "<<this_thread::get_id()<<" locked mutex again"<<endl;
mtx.unlock();//解鎖一次
cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex"<<endl;
mtx.unlock();//再次解鎖
cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex again"<<endl;
}
int main()
{
std::thread t1(recursive_function);
t1.join();
return 0;
}3.std::timed_mutex
- 這是一個帶時限的互斥量
- 除了提供基本的鎖定和解鎖操作外,它還允許線程嘗試在一定時間內(nèi)鎖定互斥量。
- 如果在指定時間內(nèi)無法獲取鎖,try_lock_for()或try_lock_until()函數(shù)將返回失敗,而線程則不會被阻塞。
- 這對于實現(xiàn)有超時機制的資源訪問非常有用。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
std::timed_mutex mtx;
void timed_lock_function() {
auto start = std::chrono::high_resolution_clock::now();//高精度時間
//嘗試在指定時間內(nèi)獲取鎖
if(mtx.try_lock_for(std::chrono::seconds(2)))//加了一個2秒的等待時間
{
cout<<"Thread"<<this_thread::get_id()<<" get the lock"<<endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
mtx.unlock();
cout<<"Thread"<<this_thread::get_id()<<" release the lock"<<endl;
}
else
{
cout<<"Thread"<<this_thread::get_id()<<" can't get the lock"<<endl;
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end-start;
cout<<"Thread"<<this_thread::get_id()<<" cost "<<diff.count()<<"s"<<endl;
}
int main()
{
std::thread t1(timed_lock_function);
std::thread t2(timed_lock_function);
t1.join();
t2.join();
return 0;
}4.std::recursive_timed_mutex:
- 這是以惡搞遞歸且?guī)r限的互斥量
- 它結(jié)合了std::recursive_mutex和std::timed_mutex的特性
- 它允許同一線程多次鎖定同一個互斥量,并提供了帶時限的鎖定嘗試功能。
- 這使得線程在需要遞歸訪問資源且希望在一定時間內(nèi)獲取鎖的場景中更加靈活。
在使用這些互斥量類型時,需要注意正確的管理鎖定和解鎖操作,以避免死鎖和其他同步問題。同時,根據(jù)具體的應(yīng)用場景和需求選擇合適的互斥量類型也是非常重要的。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
基于VC中使用ForceInclude來強制包含stdafx.h的解決方法
本篇文章是對VC中使用ForceInclude來強制包含stdafx.h的解決方法進行了詳細的分析介紹,需要的朋友參考下2013-05-05
C/C++通過SQLite SDK實現(xiàn)數(shù)據(jù)庫增刪改查操作
SQLite,作為一款嵌入式關(guān)系型數(shù)據(jù)庫管理系統(tǒng),一直以其輕量級、零配置以及跨平臺等特性而備受青睞,本文主要介紹了C++如何通過SQLite SDK實現(xiàn)數(shù)據(jù)庫增刪改查操作,感興趣的可以了解下2023-11-11
使用C語言中的time函數(shù)獲取系統(tǒng)時間
在C語言中可以使用time函數(shù)來獲取系統(tǒng)時間,以下對time函數(shù)進行了介紹,需要的朋友可以過來參考下2013-07-07

