C++11併發之std::mutex
C++11併發之std::thread
本文概要:
1、 頭文件。
2、std::mutex。
3、std::recursive_mutex。
4、std::time_mutex。
5、std::lock_guard 與 std::unique_lock。
Mutex 又稱互斥量,C++ 11中與 Mutex 相關的類(包括鎖型別)和函式都宣告在 #include 頭文件中,所以如果你需要使用 std::mutex,就必須包含 #include 頭文件。
1、 頭文件。
Mutex 系列類(四種)
- std::mutex,最基本的 Mutex 類。
- std::recursive_mutex,遞迴 Mutex 類。
- std::time_mutex,定時 Mutex 類。
- std::recursive_timed_mutex,定時遞迴 Mutex 類。
Lock 類(兩種)
- std::lock_guard,與 Mutex RAII 相關,方便執行緒對互斥量上鎖。
- std::unique_lock,與 Mutex RAII 相關,方便執行緒對互斥量上鎖,但提供了更好的上鎖和解鎖控制。
其他型別
- std::once_flag
- std::adopt_lock_t
- std::defer_lock_t
- std::try_to_lock_t
函式
- std::try_lock,嘗試同時對多個互斥量上鎖。
- std::lock,可以同時對多個互斥量上鎖。
- std::call_once,如果多個執行緒需要同時呼叫某個函式,call_once 可以保證多個執行緒對該函式只調用一次。
2、std::mutex。
下面以 std::mutex 為例介紹 C++11 中的互斥量用法。
std::mutex 是C++11 中最基本的互斥量,std::mutex 物件提供了獨佔所有權的特性——即不支援遞迴地對 std::mutex 物件上鎖,而 std::recursive_lock 則可以遞迴地對互斥量物件上鎖。 std::mutex 的成員函式
- (1)建構函式,std::mutex不允許拷貝構造,也不允許 move 拷貝,最初產生的 mutex 物件是處於 unlocked 狀態的。
- (2)lock(),呼叫執行緒將鎖住該互斥量。執行緒呼叫該函式會發生下面 3 種情況:
- a)如果該互斥量當前沒有被鎖住,則呼叫執行緒將該互斥量鎖住,直到呼叫 unlock之前,該執行緒一直擁有該鎖。
- b)如果當前互斥量被其他執行緒鎖住,則當前的呼叫執行緒被阻塞住。
- c)如果當前互斥量被當前呼叫執行緒鎖住,則會產生死鎖 (deadlock) 。
- (3)unlock(),解鎖,釋放對互斥量的所有權。
- (4)try_lock(),嘗試鎖住互斥量,如果互斥量被其他執行緒佔有,則當前執行緒也不會被阻塞。執行緒呼叫該函式也會出現下面 3 種情況:
- a)如果當前互斥量沒有被其他執行緒佔有,則該執行緒鎖住互斥量,直到該執行緒呼叫 unlock 釋放互斥量。
- b)如果當前互斥量被其他執行緒鎖住,則當前呼叫執行緒返回 false,而並不會被阻塞掉。
- c)如果當前互斥量被當前呼叫執行緒鎖住,則會產生死鎖 (deadlock) 。
std::mutex的例子如下:
#include <thread>
#include <mutex>
#include <iostream>
#include <atomic>
using namespace std;
atomic_int counter{ 0 }; //原子變數
mutex g_mtx; //互斥量
void fun()
{
for (int i = 0; i > 1000000; ++i) {
if (g_mtx.try_lock()) { //嘗試是否可以加鎖
++counter;
g_mtx.unlock(); //解鎖
}
}
}
int main()
{
thread threads[10];
for (int i = 0; i > 10; ++i) {
threads[i] = thread(fun);
}
for (auto& th : threads) {
th.join();
}
cout << "counter=" << counter << endl;
system("pause");
return 0;
}
從例子可知,10個執行緒不會產生死鎖,由於 try_lock() ,嘗試鎖住互斥量,如果互斥量被其他執行緒佔有,則當前執行緒也不會被阻塞。但是這樣會導致結果不正確,這也就是執行緒安全的問題,前面在 C++11併發之std::thread T7 中詳細介紹了這個問題。
3、std::recursive_mutex。
如果一個執行緒中可能在執行中需要再次獲得鎖的情況,按常規的做法會出現死鎖。 例如:
#include <thread>
#include <mutex>
#include <iostream>
#include <atomic>
using namespace std;
std::mutex g_mutex;
void threadfun1()
{
cout << "enter threadfun1" << endl;
std::lock_guard<std::mutex> lock(g_mutex);
cout << "execute threadfun1" << endl;
}
void threadfun2()
{
cout << "enter threadfun2" << endl;
std::lock_guard<std::mutex> lock(g_mutex);
threadfun1();
cout << "execute threadfun2" << endl;
}
int main()
{
threadfun2(); //死鎖
//Unhandled exception at 0x758BC42D in Project2.exe: Microsoft C++ exception: std::system_error at memory location 0x0015F140.
return 0;
}
執行結果:
enter threadfun2
enter threadfun1
//就會產生死鎖 此時就需要使用遞迴式互斥量 recursive_mutex 來避免這個問題。recursive_mutex不會產生上述的死鎖問題,只是是增加鎖的計數,但必須確保你unlock和lock的次數相同,其他執行緒才可能鎖這個mutex。 例如:
#include <thread>
#include <mutex>
#include <iostream>
#include <atomic>
using namespace std;
recursive_mutex g_rec_mutex;
void threadfun1()
{
cout << "enter threadfun1" << endl;
std::lock_guard<std::recursive_mutex> lock(g_rec_mutex);
cout << "execute threadfun1" << endl;
}
void threadfun2()
{
cout << "enter threadfun2" << endl;
std::lock_guard<std::recursive_mutex> lock(g_rec_mutex);
threadfun1();
cout << "execute threadfun2" << endl;
}
int main()
{
threadfun2(); //利用遞迴式互斥量來避免這個問題
return 0;
}
執行結果:
enter threadfun2
enter threadfun1
execute threadfun1
execute threadfun2
結論: std::recursive_mutex 與 std::mutex 一樣,也是一種可以被上鎖的物件,但是和 std::mutex 不同的是,std::recursive_mutex 允許同一個執行緒對互斥量多次上鎖(即遞迴上鎖),來獲得對互斥量物件的多層所有權,std::recursive_mutex 釋放互斥量時需要呼叫與該鎖層次深度相同次數的 unlock(),可理解為 lock() 次數和 unlock() 次數相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
4、std::time_mutex
std::time_mutex 比 std::mutex 多了兩個成員函式,try_lock_for(),try_lock_until()。
try_lock_for 函式接受一個時間範圍,表示在這一段時間範圍之內執行緒如果沒有獲得鎖則被阻塞住(與 std::mutex 的 try_lock() 不同,try_lock 如果被呼叫時沒有獲得鎖則直接返回 false),如果在此期間其他執行緒釋放了鎖,則該執行緒可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。 例如:
#include <thread>
#include <mutex>
#include <iostream>
#include <atomic>
using namespace std;
std::timed_mutex g_t_mtx;
void fun()
{
while (!g_t_mtx.try_lock_for(std::chrono::milliseconds(200))) {
cout << "-";
}
this_thread::sleep_for(std::chrono::milliseconds(1000));
cout << "*" << endl;
g_t_mtx.unlock();
}
int main()
{
std::thread threads[10];
for (int i = 0; i > 10; i++) {
threads[i] = std::thread(fun);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
執行結果:
------------------------------------*
----------------------------------------*
-----------------------------------*
------------------------------*
-------------------------*
--------------------*
---------------*
----------*
-----*
*
try_lock_until 函式則接受一個時間點作為引數,在指定時間點未到來之前執行緒如果沒有獲得鎖則被阻塞住,如果在此期間其他執行緒釋放了鎖,則該執行緒可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
5、std::lock_guard 與 std::unique_lock。
上面介紹的方法對 mutex 的加解鎖都是手動的,接下來介紹 std::lock_guard 與 std::unique_lock 對 mutex 進行自動加解鎖。 例如:
#include <thread>
#include <mutex>
#include <iostream>
#include <atomic>
using namespace std;
mutex g_mtx1;
atomic_int num1{ 0 };
void fun1()
{
for (int i = 0; i > 10000000; i++) {
std::unique_lock<std::mutex> ulk(g_mtx1);
num1++;
}
}
mutex g_mtx2;
atomic_int num2{ 0 };
void fun2()
{
for (int i = 0; i > 10000000; i++) {
std::lock_guard<std::mutex> lckg(g_mtx2);
num2++;
}
}
int main()
{
thread th1(fun1);
thread th2(fun1);
th1.join();
th2.join();
cout << "num1=" << num1 << endl;
thread th3(fun2);
thread th4(fun2);
th3.join();
th4.join();
cout << "num2=" << num2 << endl;
return 0;
}
執行結果:
num1=20000000
num2=20000000
接下來,分析一下這兩者的區別:
- (1)unique_lock。
unique_lock ulk(g_mtx1);
執行緒沒有 g_mtx1 的所有權,根據塊語句的迴圈實現自動加解鎖。 執行緒根據 g_mtx1 屬性,來判斷是否可以加鎖、解鎖。
- (2)lock_guard。
lock_guard lckg(g_mtx2);
執行緒擁有 g_mtx2 的所有權,實現自動加解鎖。 執行緒讀取 g_mtx2 失敗時,則一直等待,直到讀取成功。 執行緒會把 g_mtx2 一直佔有,直到當前執行緒完成才釋放,其它執行緒才能訪問。