@
一、基本概念
在多線程環境中,有多個線程競爭同一個公共資源,就很容易引發線程安全的問題。因此就需要引入鎖的機制,來保證任意時候只有一個線程在訪問公共資源。
- 互斥量就是個類對象,可以理解為一把鎖,多個線程嘗試用lock()成員函數來加鎖,只有一個線程能鎖定成功,如果沒有鎖成功,那么流程將卡在lock()這里不斷嘗試去鎖定。
- 互斥量使用要小心,保護數據不多也不少,少了達不到效果,多了影響效率。
二、使用方法
包含頭文件#include <mutex>
2.1 mutex.lock(),unlock()
步驟:1.lock(),2.操作共享數據,3.unlock()。
lock()和unlock()要成對使用,不能重復上鎖和解鎖。本質就是lock~unlock之間的程序(數據)不會同時調用、修改。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
list<int> test_list;
mutex my_mutex;
void in_list(){
for(int num=0;num<1000000000000000000;num++){
my_mutex.lock();
cout<<"插入數據: "<<num<<endl;
test_list.push_back(num);
my_mutex.unlock();
}
}
void out_list(){
for(int num=0;num<1000000000000000000; ++num){
my_mutex.lock();
if(!test_list.empty()){
int tmp = test_list.front();
test_list.pop_front();
cout<<"取出數據:"<<tmp<<endl;
}
my_mutex.unlock();
}
}
int main()
{
thread in_thread(in_list);
thread out_thread(out_list);
in_thread.join();
out_thread.join();
cout << "Hello World!" << endl;
return 0;
}
2.2 std::lock_guard類模板
lock_guard構造函數執行了mutex::lock(),在作用域結束時,自動調用析構函數,執行mutex::unlock()
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
list<int> test_list;
mutex my_mutex;
void in_list(){
for(int num=0;num<1000000000000000000;num++){
std::lock_guard<std::mutex> my_guard(my_mutex);
cout<<"插入數據: "<<num<<endl;
test_list.push_back(num);
}
}
void out_list(){
for(int num=0;num<1000000000000000000; ++num){
std::lock_guard<std::mutex> my_guard(my_mutex);
if(!test_list.empty()){
int tmp = test_list.front();
test_list.pop_front();
cout<<"取出數據:"<<tmp<<endl;
}
}
}
int main()
{
thread in_thread(in_list);
thread out_thread(out_list);
in_thread.join();
out_thread.join();
cout << "Hello World!" << endl;
return 0;
}
2.2.1 std::lock_guard的std::adopt_lock參數
std::lock_guard<std::mutex> my_guard(my_mutex,std::adopt_lock);
加入adopt_lock后,在調用lock_guard的構造函數時,不再進行lock();
adopt_guard為結構體對象,起一個標記作用,表示這個互斥量已經lock(),不需要在lock()。
2.3 std::unique_lock函數模板
unique_lock想比於lock_guard,都是基於RAII思想的,也支持std::lock_guard的功能,但是區別在於它提供更多的成員函數,比如:lock(),unlock()使用更加靈活,並且可以和condiction_variable一起使用控制線程同步。但是效率差一點,內存占用多一點。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
list<int> test_list;
mutex my_mutex;
void in_list(){
for(int num=0;num<1000000000000000000;num++){
std::unique_lock<std::mutex> my_guard(my_mutex);
cout<<"插入數據: "<<num<<endl;
test_list.push_back(num);
}
}
void out_list(){
for(int num=0;num<1000000000000000000; ++num){
std::unique_lock<std::mutex> my_guard(my_mutex);
if(!test_list.empty()){
int tmp = test_list.front();
test_list.pop_front();
cout<<"取出數據:"<<tmp<<endl;
}
}
}
int main()
{
thread in_thread(in_list);
thread out_thread(out_list);
in_thread.join();
out_thread.join();
cout << "Hello World!" << endl;
return 0;
}
2.3.1 unique_lock的第二個參數
1) std::adopt_lock:
- 表示這個互斥量已經被lock(),即不需要在構造函數中lock這個互斥量了。
- 前提:必須提前lock
- lock_guard中也可以用這個參數
2) std::try_to_lock:
- 嘗試用mutx的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即返回,不會阻塞在那里,但也不能操作保護的數據(防止異常),只能操作不受保護的數據;
- 使用try_to_lock的原因是防止其他的線程鎖定mutex太長時間,導致本線程一直阻塞在lock這個地方
- 前提:不能提前lock();
- unique_lock.owns_locks()方法判斷是否拿到鎖,如拿到返回true
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
list<int> test_list;
mutex my_mutex;
void in_list(){
for(int num=0;num<10000;num++){
std::unique_lock<std::mutex> my_unique(my_mutex, std::try_to_lock);
if(my_unique.owns_lock()){
cout<<"插入數據: "<<num<<endl;
test_list.push_back(num);
}
else{
cout<<"沒能拿到鎖,只能干點別的事"<<endl;
}
}
}
void out_list(){
for(int num=0;num<10000; ++num){
std::unique_lock<std::mutex> my_unique(my_mutex);
std::chrono::seconds dura(1);
std::this_thread::sleep_for(dura);
if(!test_list.empty()){
int tmp = test_list.front();
test_list.pop_front();
cout<<"取出數據:"<<tmp<<endl;
}
else {
cout<<"已經空了"<<endl;
}
}
}
int main()
{
thread in_thread(in_list);
thread out_thread(out_list);
in_thread.join();
out_thread.join();
cout << "Hello World!" << endl;
return 0;
}
3) std::defer_lock:
- 加上defer_lock是始化了一個沒有加鎖的mutex
- 不給它加鎖的目的是以后可以調用后面提到的unique_lock的一些方法
- 前提:不能提前lock
2.3.2 unique_lock的成員函數
(前三個與std::defer_lock聯合使用)
1)lock():加鎖
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
作用是:不用自己unlock();
2)unlock():解鎖。
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//處理一些共享代碼
myUniLock.unlock();
//處理一些非共享代碼
myUniLock.lock();
//處理一些共享代碼
作用:因為一些非共享代碼要處理,可以暫時先unlock(),用其他線程把它們處理了,處理完后再lock()。
3)try_lock():嘗試給互斥量加鎖
如果拿不到鎖,返回false,否則返回true。用法和前面的try_to_lock參數一致。
4)release():釋放unique_lock所管理的mutex對象指針
myUniLock(myMutex)
相當於把myMutex和myUniLock綁定在了一起,release()就是解除綁定,返回它所管理的mutex對象的指針,並釋放所有權- 用法
mutex* ptx =myUniLock.release()
:
所有權由ptx接管,如果原來mutex對象處理加鎖狀態,就需要自己進行解鎖了。ptx->unlock();
2.3.3 所有權轉移
- 使用move轉移
unique_lock myUniLock(myMutex);
把myMutex和myUniLock綁定在了一起,也就是myUniLock擁有myMutex的所有權
// myUniLock擁有myMutex的所有權,myUniLock可以把自己對myMutex的所有權轉移,但是不能復制。
unique_lock myUniLock2(std::move(myUniLock));
//現在myUniLock2擁有myMutex的所有權。
- 在函數中return一個臨時變量,即可以實現轉移
unique_lock<mutex> aFunction()
{
unique_lock<mutex> myUniLock(myMutex);
//移動構造函數那里講從函數返回一個局部的unique_lock對象是可以的
//返回這種局部對象會導致系統生成臨時的unique_lock對象,並調用unique_lock的移動構造函數
return myUniLock;
}
2.4 std::lock()函數模板
std::lock(mutex1,mutex2……)
:一次鎖定多個互斥量(一般這種情況很少),用於處理多個互斥量。
如果有一個沒鎖住,就會把已經鎖住的釋放掉,然后它就等待,等所有互斥量都可以同時鎖住,才繼續執行。(要么互斥量都鎖住,要么都沒鎖住,防止死鎖)
三、死鎖
3.1 發生原因
死鎖至少有兩個互斥量mutex1,mutex2。
- 線程A執行時,這個線程先鎖mutex1,並且鎖成功了,然后去鎖mutex2的時候,出現了上下文切換。
- 線程B執行,這個線程先鎖mutex2,因為mutex2沒有被鎖,即mutex2可以被鎖成功,然后線程B要去鎖mutex1.
- 此時,死鎖產生了,A鎖着mutex1,需要鎖mutex2,B鎖着mutex2,需要鎖mutex1,兩個線程沒辦法繼續運行下去。。。
3.2 解決辦法
只要保證多個互斥量上鎖的順序一樣就不會造成死鎖。
四、鎖的效率
lock的代碼段越少,執行越快,整個程序的運行效率越高。
- 鎖住的代碼少,叫做粒度細,執行效率高;
- 鎖住的代碼多,叫做粒度粗,執行效率低;