C++11有關線程同步的使用
本文鏈接:https://blog.csdn.net/fengxinlinux/article/details/76686829
互斥量和條件變量是控制線程同步的常用手段,用來保護多線程同時訪問的共享數據。
c++11提供了這些操作,同時還提供了原子變量和一次調用的操作,用起來非常的方便。
我們在這裏只介紹如何在C++中使用這些同步機制,有關概念的介紹我們就不在這裏多說了。
互斥量
C++11中提供瞭如下4種語義的互斥量(mutex):
- std::mutex:獨佔的互斥量,不能遞歸使用。
- std::timed_mutex:帶超時的獨佔互斥量,不能遞歸使用。
- std::recursive_mutex:遞歸互斥量,不帶超時功能。
- std::recursive_timed_mutex:帶超時的遞歸互斥量。
獨佔互斥量std::mutex
這些互斥量的基本接口很相似,一般用法是通過lock()方法來阻塞線程,直到獲得互斥量的所有權爲止。在線程獲得互斥量並完成任務之後,就必須使用unlock()來解除對互斥量的佔用,lock()和unlock()必須成對出現。try_lock()嘗試鎖定互斥量,如果成功則返回true,如果失敗則返回false,它是非阻塞的。
std::mutex的基本用法代碼如下:
#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
using namespace std;
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(1));
std::cout<<"leaving thread "<<std::this_thread::get_id()<<std::endl;
g_lock.unlock();
}
int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
t1.join();
t2.join();
t3.join();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
使用lock_guard可以簡化lock/unlock的寫法,同時也更安全,因爲lock_guard在構造時會自動鎖定互斥量,而在退出作用域後進行析構時就會自動解鎖,從而保證了互斥量的正確操作,避免忘記unlock操作,因此,應儘量用lokc_guard。lock_guard用到了RAII技術,這種技術在類的構造函數中分配資源,在析構函數中釋放資源,保證資源在出了作用域之後就釋放。上面的例子使用lock_guard後會更簡潔,代碼如下:
void func()
{
lock_guard<std::mutex> locker(g_lock); //出作用域之後自動解鎖
cout<<"entered thread "<<this_thread::get_id()<<endl;
this_thread::sleep_for(std::chrono::seconds(1));
cout<<"leaving thread "<<std::this_thread::get_id()<<endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
遞歸的獨佔互斥量std::recursive_mutex
遞歸鎖允許同一線程多次獲得該互斥鎖,可以用來解決同一線程需要多次獲取互斥量時死鎖的問題。一個線程多次獲取同一個互斥量時會發生死鎖。要解決這個死鎖的問題,一個簡單的辦法就是用遞歸鎖:std::recursive_mutex,它允許同一線程多次獲得互斥量。
代碼示例:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
struct Complex
{
std::recursive_mutex mutex;
int i;
Complex():i(0){}
void mul(int x)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
i*=x;
}
void div(int x)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
i/=x;
}
void both(int x,int y)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
mul(x);
div(y);
}
};
int main()
{
Complex complex;
complex.both(32,23); //因爲同一線程可以多次獲取同一互斥量,不會發生死鎖
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
需要注意的是儘量不要使用遞歸鎖好,主要原因如下:
- 需要用到遞歸鎖定的多線程互斥處理往往本身就是可以簡化的,允許遞歸互斥很容易放縱複雜邏輯產生,從而導致一些多線程同步引起的晦澀問題。
- 遞歸鎖比起非遞歸鎖,效率會低一些。
- 遞歸鎖雖然允許同一個線程多次獲得同一互斥量,可重複獲得的最大次數並未具體說明,一旦超過一定次數,再對lock進行調用就會拋出std::system錯誤。
帶超時的互斥量
std::timed_mutex是超時的獨佔鎖,std::recursive_timed_mutex是超時的遞歸鎖,主要用在獲取鎖時超時等待功能,因爲有時不知道獲取鎖需要多久,爲了不至於一直在等待獲取互斥量,就設置一個等待超時時間,在超時後還可以做其他事情。
std::timed_mutex比std::mutex多了兩個超時獲取鎖的接口:try_lock_for和try_lock_until,這兩個接口是用來設置獲取互斥量的超時時間。
std::timed_mutex的基本用法如下代碼:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
std::timed_mutex mutex1;
void work()
{
chrono::milliseconds timeout(100);
while(true)
{
if(mutex1.try_lock_for(timeout))
{
cout<<this_thread::get_id()<<":do work with the mutex"<<endl;
chrono::milliseconds sleepDuration(250);
this_thread::sleep_for(sleepDuration);
mutex1.unlock();
this_thread::sleep_for(sleepDuration);
}
else
{
cout<<this_thread::get_id()<<": do work without mutex"<<endl;
chrono::milliseconds sleepDuration(100);
this_thread::sleep_for(sleepDuration);
}
}
}
int main()
{
thread t1(work);
thread t2(work);
t1.join();
t2.join();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
在上面的例子中,通過一個while循環不斷地去獲取超時鎖,如果超時還沒有獲取到鎖時就休眠100毫秒,再繼續獲取超時鎖。
相比std::timed_mutex,std::recursive_timed_mutex多了遞歸鎖的功能,允許同一線程多次獲得互斥量。
條件變量
條件變量是C++11提供的另外一種用於等待的同步機制,它能阻塞一個或多個線程,直到收到另外一個線程發出的通知或者超時,纔會喚醒當前阻塞的線程。條件變量需要和互斥量配合起來用。C++11提供了兩種條件變量:
- condition_variable,配合std::unique_lock<std::mutex>進行wait操作。
- condition_variable_any,和任意帶有lock、unlock語義的mutex搭配使用,比較靈活,但效率比condition_variable差一些。
可以看到condition_variable_any比condition_variable更靈活,因爲它更通用,對所有的鎖都適用,而condition_variable性能更好。
條件變量的使用過程如下:
- 擁有條件變量的線程獲取互斥量。
- 循環檢查某個條件,如果條件不滿足,則阻塞直到條件滿足;如果條件滿足,則向下執行。
- 某個線程滿足條件執行完之後調用notify_one或notify_all喚醒一個或者所有的等待線程。
我們可以看一個經典的生產者-消費者的例子:
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<unistd.h>
using namespace std;
mutex m_mutex; //互斥量
int s=0; //共享資源
condition_variable m_notempty; //條件變量
void producer() //生產者
{
while(1)
{
sleep(1);
m_mutex.lock(); //上鎖
s++;
cout<<"increase one ,s="<<s<<endl;
m_mutex.unlock();
m_notempty.notify_one();
}
}
void consumer() //消費者
{
while(1)
{
sleep(1);
unique_lock<mutex> locker(m_mutex);
while(s==0)
m_notempty.wait(locker); //使用條件變量
s--;
cout<<"decrease one,s="<<s<<endl;
}
}
int main()
{
//創建線程
thread thread1(producer);
thread thread2(consumer);
thread1.join();
thread2.join();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
上面的例子中,while(s==0) m_notempty.wait(locker);
,這句代碼的意思的是當s等於0的時候,阻塞直到條件滿足時被喚醒。
我們也可以這麼用, m_notempty.wait(locker,[]{return s>0;});
,將判斷條件放到函數裏面,意思是wait將一直阻塞,知道判斷條件滿足時,被喚醒。
原子變量
C++11提供了一個原子類型std::atomic<T>,可以使用任意類型作爲模板參數,C++11內置了整型的原子變量,可以更方便地使用原子變量,使用原子變量就不需要使用互斥量來保護該變量了,因爲對該變量的操作保證其是原子的,是不可中斷的。用起來更簡潔。
要做一個計時器,使用mutex時,代碼如下:
#include<iostream>
#include<mutex>
using namespace std;
struct Counter
{
public:
int value;
std::mutex m_mutex;
void increment()
{
std::lock_guard<std::mutex> lock(mutex);
value++;
}
void decrement()
{
std::lock_guard<std::mutex> lock(mutex);
value--;
}
int get()
{
return value;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
如果使用原子變量,就不需要再定義互斥量了,使用更簡便。
#include<iostream>
#include<mutex>
using namespace std;
struct Counter
{
public:
std::atomic<int> value;
void increment()
{
value++;
}
void decrement()
{
value--;
}
int get()
{
return value;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
call_one/once_flag的使用
爲了保證在多線程環境中某個函數僅被調用一次,比如,需要初始化某個對象,而這個對象只能初始化一次時,就可以用std::call_once來保證函數在多線程環境中只被調用一次。使用std::call_once時,需要一個once_flag作爲call_one的入參,它的用法比較簡單。
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
std::once_flag flag;
void do_once()
{
std::call_once(flag,[]{std::cout<<"Called once"<<endl;});
}
int main()
{
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
t1.join();
t2.join();
t3.join();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
運行結果:
Called once