標准線程庫
C++11 新標准中引入了幾個頭文件來支持多線程編程:
< thread > :包含std::thread類以及std::this_thread命名空間。管理線程的函數和類在 中聲明.
< atomic > :包含std::atomic和std::atomic_flag類,以及一套C風格的原子類型和與C兼容的原子操作的函數。
< mutex > :包含了與互斥量相關的類以及其他類型和函數
< future > :包含兩個Provider類(std::promise和std::package_task)和兩個Future類(std::future和std::shared_future)以及相關的類型和函數。
< condition_variable > :包含與條件變量相關的類,包括std::condition_variable和std::condition_variable_any。
std::thread
在C++11中可以簡單地使用std:thread類來創建線程,構造thread對象時傳入一個可調用對象作為參數(如果帶參,則將參數也同時傳入),可調用對象包括函數、函數指針、lambda表達式、bind創建的對象或重載了函數調用運算符的對象,例如:
/*無參*/
void thread01(){
/*....*/
}
/*含參*/
void thread02(int i,int j){
/*...*/
}
...
int main(){
/*...*/
thread first(thread01);/*構造thread對象完成后新的線程立馬被創建。同時可調用對象被調用*/
thread second(thread02,10,10);/*傳入參數,可調用對象被調用*/
}
若采用std:thread的默認構造函數來構造對象,則該thread不關聯任何線程,可以利用joinable()來查看一個thread對象是否關聯某個線程
int main(){
/*...*/
thread first(thread01);/*構造thread對象完成后新的線程立馬被創建。同時該可調用對象立馬被調用*/
thread second(thread02,10,10);/*傳入參數*/
thread third;/*沒有綁定*/
cout<<first.joinable()<< " "<<second.joinable()<<" "<<third.joinable()<<endl;
}
運行結果:1 1 0
join 匯聚
thread類調用join()函數后,原始線程會等待新線程執行完畢之后再去銷毀線程對象。這樣,如果新線程使用了原始線程中的某個共享對象不會發生異常,在新線程結束后才會析構共享對象;若新線程和主線程分離,主線程析構了某個對象或變量后,新線程再去調用的話則會產生異常。
thread::join()還會在進程結束后清理子線程相關的內存空間,此后該thread對象不再與該子線程相關,即thread::joinable() = false ,故join對一個線程而言只能調用一次
detach 分離
thread::detach將線程從thread對象分離出來,運行線程獨立執行,此時即使主線程結束了子線程還有可能在后台運行。通常稱分離線程為守護線程(daemon threads),UNIX中守護線程是指,沒有任何顯式的用戶接口,並在后台運行的線程。
由於detach不像join一樣會等待子線程與主線程匯聚,故要注意以下幾點:
- 如果傳遞int這種簡單類型,推薦使用值傳遞,不要用引用
- 如果傳遞類對象,避免使用隱式類型轉換,全部都在創建線程這一行就創建出臨時對象,然后在函數參數里,用引用來接,否則還會創建出一個對象。
std::mutex
考慮到多個線程對同一個對象進行操作,若全為只讀操作,則數據是安全的,若有讀有寫,則很容易發生錯誤,因此要使用互斥量(mutex)來保護數據。
互斥量就是個類對象,可以理解為一把鎖,多個線程嘗試用lock()成員函數來加鎖,只有一個線程能鎖定成功,如果沒有鎖成功,那么線程將阻塞在lock()這里不斷嘗試去鎖定,直到lock成功。
lock()&unlock()
實例分析:
未加鎖狀態
#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
int num = 0;
mutex mtx;
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
// mtx.lock();
++num;
// mtx.unlock();
}
}
void thread02()
{
for (int i = 0; i < 1000000; i++)
{
// mtx.lock();
++num;
// mtx.unlock();
}
}
int main()
{
thread task01(thread01);
thread task02(thread02);
task01.join();
task02.join();
cout << "The num is " << num << endl;
system("pause");
return 0;
}
多次運行程序結果:
可以看到每次運行的結果都與預期的結果不一樣,數據未加鎖則會引起數據沖突。下面是加鎖狀態:
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
mtx.lock();//加鎖
++num;
mtx.unlock();//解鎖
}
}
void thread02()
{
for (int i = 0; i < 1000000; i++)
{
mtx.lock();
++num;
mtx.unlock();
}
}
//其他與上面的例子相同
程序運行結果:
這次程序運行的結果是正確的,可以看出數據加鎖的重要性。不過由於加鎖會導致阻塞,因此保護數據多了會影響程序運行的效率。
try_lock()
try_lock()會試圖去lock mutex對象,但是不會造成阻塞,通常有以下幾種情況:
- 如果mutex沒有被任何線程lock,則調用線程會lock
- 如果mutex被其他線程lock,則該函數會失敗,返回false,但是不會造成阻塞。
- 如果mutex被與調用該函數的線程相同的線程lock,則會產生一個死鎖,因為mutex不支持遞歸,一般情況下會崩潰。
std::lock_guard
std::lock_guard用於管理mutex,很顯然對於mutex的lock()和unlock()要成對調用,如果lock()之后忘記unlock()會出現死鎖或其他很嚴重的錯誤,而有時程序會有很多出口,例如break
continue
return
,有時還會有異常拋出,在這些出口前都加上unlock()很麻煩。
std::lock_guard是mutex的封裝器,創建lock_guard對象時,它會試圖接收給定mutex的所有權。當線程離開創建lock_guard對象所在的作用域時,lock_guard對象被自動析構並釋放mutex,我們可以將上述thread01()改為:
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
std::lock_guard<std::mutex> guard(mtx);//利用lock_guard管理mutex
++num;
}
}
std::unique_lock
std::unique_lock 與std::lock_guard都能實現自動加鎖與解鎖功能,但是std::unique_lock要比std::lock_guard更靈活,但是更靈活的代價是占用空間相對更大一點且相對更慢一點。此處不展開說明。