C++11多執行緒基本使用
C++11增加了執行緒及執行緒相關的累,很方便的支援了併發程式設計,使得編寫的多執行緒程式的可移植性得到了很大的提高.
執行緒的建立
用std::thread 建立執行緒非常的簡單,只需要提供執行緒函式或者函式物件即可,並可以同時指定執行緒的引數:
#include<iostream>
#include<thread>
#include<chrono>
using namespace std;
//執行緒函式
void func(int a, int b, int c)
{
std::this_thread::sleep_for(std::chrono::seconds(3 ));
cout << a << " " << b << " " << c << endl;
}
int main()
{
//建立執行緒物件t1,繫結執行緒函式為func
std::thread t1(func, 1, 2, 3);
//輸出t1的執行緒ID
std::cout << "ID:" << t1.get_id() << std::endl;
//等待t1執行緒函式執行結束
t1.join();
std::thread t2(func, 2 , 3, 4);
//後臺執行t2的執行緒函式,並且不會因為main函式結束時,執行緒函式未執行完而產生異常
t2.detach();
cout << "after t2 ,main is runing" << endl;
//以lambda表示式作為被幫頂的執行緒函式
std::thread t4([](int a, int b, int c)
{
//執行緒休眠5秒
std::this_thread::sleep_for(std::chrono::seconds(5));
cout << a << " " << b << " " << c << endl;
}, 4,5,6);
t4.join();
//獲取CPU的核數
cout << "CPU: " << thread::hardware_concurrency() << endl;
//當新增下面註釋掉的語句會丟擲異常,因為執行緒物件先於執行緒函式結束了,應該保證執行緒物件的生命週期線上程函式執行完時仍然存在.
//std::thread t3(func, 3, 4, 5);
return 0;
}
執行緒函式將會運行於執行緒物件t中,join函式將會阻塞執行緒,直到執行緒函式執行結束,如果執行緒函式有返回值,返回值將被忽略.
detach可以將執行緒與執行緒物件分離,讓執行緒作為後臺執行緒執行,當前執行緒也不會阻塞了.但是detach之後就無法在和執行緒發生聯絡了.如果執行緒執行函式使用了臨時變數可能會出現問,執行緒呼叫了detach在後臺執行,臨時變數可能已經銷燬,那麼執行緒會訪問已經被銷燬的變數.join能保證.
雖然這種方式建立執行緒很方便,但是std::thread 出了作用域後將會析構,這個時候執行緒函式還沒執行完則會發生錯誤.
執行緒不可以複製但是可以移動.但是執行緒移動後,執行緒物件將不再代表任何執行緒了:
std::thread t(func); //移動後,執行緒物件t不在代表任何執行緒 std::thread t1(std::move(t)); // t.join(); t1.join();
互斥量
互斥量是一種同步原語,是一種執行緒同步的手段,用來保護多執行緒同時訪問的共享資料.
std::mutex: 獨佔的互斥量,不能遞迴使用.
std::timed_mutex: 帶超時的獨佔互斥量,不能遞迴使用.
std::recursive_mutex: 遞迴互斥量,不帶超時功能.
std::recursive_timed_mutex: 帶超時的遞迴互斥量.
這些互斥量的基本介面十分相近,都是通過lock()來阻塞執行緒,直到獲得互斥量的所有權為止.線上程或的互斥量並完成任務後,就必須使用unlock()來解除對互斥量的佔用,lock和unlock必須成對出現.try_lock()嘗試鎖定互斥量,成功返回true,失敗返回false,他是非阻塞的.
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
std::mutex g_lock;
void func()
{
//上鎖
g_lock.lock();
cout << "in id: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "out id: " << this_thread::get_id() << endl;
//解鎖
g_lock.unlock();
}
void f()
{
//lock_guard在構造時會自動鎖定互斥量,而在退出作用域後進行析構時就會自動解鎖.
lock_guard<std::mutex> lock(g_lock);
cout << "in id: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "out id: " << this_thread::get_id() << endl;
}
int main()
{
std::thread t1(func);
std::thread t2(func);
std::thread t3(func);
t1.join();
t2.join();
t3.join();
std::thread t4(f);
std::thread t5(f);
std::thread t6(f);
t4.join();
t5.join();
t6.join();
}
lock_guard用到了RAII的技術,這種技術在類的建構函式中分配資源,在解構函式中釋放資源,保證資源在出了作用域之後就釋放.
std::recursive_mutex遞迴鎖允許同一個執行緒多次獲得互斥量.但是儘量不要使用遞迴鎖:
- 需要用到遞迴鎖定的多執行緒互斥處理往往本身就是可以簡化的,允許遞迴互很容易放縱複雜邏輯的產生,從而導致一些多執行緒同步引起的晦澀問題.
- 遞迴鎖比起非遞迴鎖,效率會低.
- 遞迴鎖雖然允許同一個執行緒多次獲得同一互斥量,但是可重複獲得的最大次數並未具體說明,一旦超過一定次數就會丟擲異常.
帶超時的互斥量在獲取鎖的時候增加了超時等待功能,因為有時不知道獲取鎖需要多久,為了不至於一直等待獲取互斥量,就設定一個等待超時時間,在超時後還可以做其他的的事情.
#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
using namespace std;
std::timed_mutex mutex1;
void work()
{
//設定阻塞時間
std::chrono::milliseconds timeout(100);
while (true) {
//帶超時的鎖,當阻塞超過100milliseconds時返回false
if (mutex1.try_lock_for(timeout)) {
cout << this_thread::get_id() << ": do work with the mutex" << endl;
std::chrono::milliseconds sleepDuration(250);
this_thread::sleep_for(sleepDuration);
} else {
cout << this_thread::get_id() << ": do work without mutex" << endl;
chrono::milliseconds sleepDuration(100);
std::this_thread::sleep_for(sleepDuration);
}
}
}
int main()
{
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
return 0;
}
條件變數
條件變數阻塞一個或多個執行緒,直到收到另外一個執行緒發來的通知或者超時,才會喚醒當前阻塞的程序,條件變數需要和互斥量配合使用.
C++11提供了兩種條件變數
- std::condition_variable,配合std::unique_lock進行wait操作
- std::condition_variable_any,和任意帶有lock,unlock的mutex進行搭配使用,比較靈活但效率略低。
條件變數的使用過程如下:
- 擁有條件變數的執行緒獲取互斥鎖
- 迴圈檢查某個條件,如果條件不滿足,則阻塞直到條件滿足,如果條件滿足,則向下執行.
- 某個執行緒滿足條件執行完之後呼叫notify_one或notify_all喚醒一個或者所有的等待執行緒.
eg:
//同步佇列的實現
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<list>
using namespace std;
template <typename T>
class SyncQueue
{
private:
//資料緩衝區
std::list<T> m_queue;
//互斥鎖
std::mutex m_mutex;
//不為滿的條件變數
std::condition_variable_any m_notFull;
//不為空的條件變數
std::condition_variable_any m_notEmpty;
//緩衝區最大大小
int m_maxsize;
//判斷是否為滿,因為給內部成員函式使用,而這些函式在呼叫前都已經上過鎖了,所以無需在加鎖
bool IsFull()
{
return m_queue.size() == m_maxsize;
}
//判斷是否為空
bool IsEmpty()
{
return m_queue.empty();
}
public:
SyncQueue(int max):m_maxsize(max) { }
//相緩衝區新增資料
void Put(const T& x)
{
//unique_lock與lock_guard相似,但是後者只能在析構時才釋放鎖,而前者可以隨時釋放鎖
std::unique_guard<std::mutex> locker(m_mutex);
//若為滿則需等待,而不能相緩衝區中新增
while (IsFull())
{
std::cout << "data Full" << std::endl;
//若為滿,訊號變數進行阻塞等待,此時釋放m_mutex鎖,然後直到被notify_one或者notify_all喚醒後先獲取m_mutex鎖
m_notFull.wait(m_mutex);
}
//相緩衝區新增資料
m_queue.push_back(x);
//喚醒處於等待中的非空條件變數
m_notEmpty.notify_one();
}
//從緩衝區獲取資料
void Take(T& x)
{
std:unique_guard<std::mutex> locker(m_mutex);
//直接使用這種方法,就無需在定義私有的Empty,也無需寫while迴圈判斷了.
//m_notEmpty.wait(locker, [this] {return !m_queue.empty();});
//若為空則需等待,而不能從緩衝區中取出
while(IsEmpty())
{
std::cout << "data Empty" << std::endl;
m_notEmpty.wait(m_mutex);
}
//獲取資料
x = m_queue.front();
//刪除被獲取的資料
m_queue.pop_front();
m_notFull.notify_one();
}
bool Empty()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.empty();
}
bool Full()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size() == m_maxsize;
}
size_t Size()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size();
}
};
原子變數
- C++11提供了一個原子型別
std::atomic<T>
,可以使用任意型別作為模板引數,C++11內建了整性的原子變數,使用原子變數就不需要使用互斥量來保護改變量了.
#include<atomic>
struct AtomicCounter {
std::atomic<int> value;
void increment()
{
++ value;
}
void decrement()
{
-- value;
}
int get()
{
return value;
}
};
call_once/once_flag
- 為了保證在多執行緒環境中某個函式僅被呼叫一次,例如,需要初始化某個物件,而這個物件智慧被初始化一次的話,就可以使用std::call_once來保證函式在多執行緒環境下只調用一次.
#include<iostream>
#include<trhead>
#include<mutex>
std:once_flag flag;
void do_once()
{
std::call_once(flag, []() {std::cout << "called" << std::endl;});
}
int main()
{
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
t1.join();
t2.join();
t3.join();
return 0;
}