天天看點

C++11多線程并發基礎知識

C++多線程并發基礎知識

1.建立線程

首先要引入頭檔案#include<thread>,C++11中管理線程的函數和類在該頭檔案中聲明,其中包括std::thread類。

例:std::thread th1(proc1)建立了一個名為th1的線程,并且線程th1開始執行。

執行個體化std::thread類對象時,至少需要傳遞函數名作為參數。如果函數為有參函數,如 void proc2(int a,int b),那麼執行個體化std::thread類對象時,則需要傳遞更多參數,參數順序依次為函數名、該函數的第一個參數、該函數的第二個參數,···,如 std::thread th2(proc2,a,b)。

join()與detach()都是std::thread類的成員函數,是兩種線程阻塞方法,兩者的差別是是否等待子線程執行結束。

等待調用線程運作結束後目前線程再繼續運作,例如,主函數中有一條語句th1.join(),那麼執行到這裡,主函數阻塞,直到線程th1運作結束,主函數再繼續運作。

隻要構造了std::thread對象(前提是,執行個體化std::thread對象時傳遞了“函數名/可調用對象”作為參數),線程就開始執行;當線程啟動後,一定要在和線程相關聯的std::thread對象銷毀前,對線程運用join()或者detach()方法。

代碼示例:

#include<iostream>
#include<thread>
using namespace std;
void proc(int &a)
{
    cout << "我是子線程,傳入參數為" << a << endl;
    cout << "子線程中顯示子線程id為" << this_thread::get_id()<< endl;
}
int main()
{
    cout << "我是主線程" << endl;
    int a = 9;
    thread th2(proc,a);//第一個參數為函數名,第二個參數為該函數的第一個參數,如果該函數接收多個參數就依次寫在後面。此時線程開始執行。
    cout << "主線程中顯示子線程id為" << th2.get_id() << endl;
    th2.join();//此時主線程被阻塞直至子線程執行結束。
    return 0;
}
           

調用join()會清理線程相關的存儲部分,這代表了join()隻能調用一次。使用joinable()來判斷join()可否調用。同樣,detach()也隻能調用一次,一旦detach()後就無法join()了,有趣的是,detach()可否調用也是使用joinable()來判斷。

2.互斥量使用

首先包含頭檔案#include<mutex>,然後需要執行個體化std::mutex對象,最後需要在進入臨界區之前對互斥量加鎖,退出臨界區時對互斥量解鎖;

2.1lock()與unlock()

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//執行個體化m對象,不要了解為定義變量
void proc1(int a)
{
    m.lock();
    cout << "proc1函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 2 << endl;
    m.unlock();
}

void proc2(int a)
{
    m.lock();
    cout << "proc2函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 1 << endl;
    m.unlock();
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}
           

需要在進入臨界區之前對互斥量lock,退出臨界區時對互斥量unlock;當一個線程使用特定互斥量鎖住共享資料時,其他的線程想要通路鎖住的資料,都必須等到之前那個線程對資料進行解鎖後,才能進行通路。不推薦直接去調用成員函數lock(),因為如果忘記unlock(),将導緻鎖無法釋放,使用lock_guard或者unique_lock則能避免忘記解鎖帶來的問題。

2.2std::lock_guard

其原理是:在其構造函數中進行加鎖,作用域結束後自動析構進行解鎖;通過使用{}來調整作用域範圍,可使得互斥量std::mutex在合适的地方被解鎖。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//執行個體化m對象,不要了解為定義變量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此語句替換了m.lock();lock_guard傳入一個參數時,該參數為互斥量,此時調用了lock_guard的構造函數,申請鎖定m
    cout << "proc1函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 2 << endl;
}//此時不需要寫m.unlock(),g1出了作用域被釋放,自動調用析構函數,于是m被解鎖

void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函數正在改寫a" << endl;
        cout << "原始a為" << a << endl;
        cout << "現在a為" << a + 1 << endl;
    }//通過使用{}來調整作用域範圍,可使得m在合适的地方被解鎖
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}
           

std::lock_gurad也可以傳入兩個參數,第二個參數為adopt_lock辨別時,表示構造函數中不再進行互斥量鎖定,是以此時需要提前手動鎖定。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//執行個體化m對象,不要了解為定義變量
void proc1(int a)
{
    m.lock();//手動鎖定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 2 << endl;
}//自動解鎖

void proc2(int a)
{
    lock_guard<mutex> g2(m);//自動鎖定
    cout << "proc2函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 1 << endl;
}//自動解鎖
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}
           

2.3std::unique_lock

std::unique_lock類似于std::lock_guard,隻是std::unique_lock用法更加豐富,同時支援std::lock_guard()的原有功能。

  • 使用std::lock_guard後不能手動lock()與手動unlock()
  • 使用std::unique_lock後可以手動lock()與手動unlock()
  • std::unique_lock的第二個參數,除了可以是adopt_lock,還可以是try_to_lock與defer_lock

try_to_lock: 嘗試去鎖定,得保證鎖處于unlock的狀态,然後嘗試現在能不能獲得鎖;嘗試用mutex的lock()去鎖定這個mutex,但如果沒有鎖定成功,會立即傳回,不會阻塞在那裡,并繼續往下執行。

defer_lock: 初始化了一個沒有加鎖的mutex。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);//始化了一個沒有加鎖的mutex
    cout << "xxxxxxxx" << endl;
    g1.lock();//手動加鎖,注意,不是m.lock();注意,不是m.lock(),m已經被g1接管了;
    cout << "proc1函數正在改寫a" << endl;
    cout << "原始a為" << a << endl;
    cout << "現在a為" << a + 2 << endl;
    g1.unlock();//臨時解鎖
    cout << "xxxxx"  << endl;
    g1.lock();
    cout << "xxxxxx" << endl;
}//自動解鎖

void proc2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//嘗試加鎖一次,但如果沒有鎖定成功,會立即傳回,不會阻塞在那裡,且不會再次嘗試鎖操作。
    if(g2.owns_lock){//鎖成功
        cout << "proc2函數正在改寫a" << endl;
        cout << "原始a為" << a << endl;
        cout << "現在a為" << a + 1 << endl;
    }else{//鎖失敗則執行這段語句
        cout <<""<<endl;
    }
}//自動解鎖

int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}
           

使用try_to_lock要小心,因為try_to_lock嘗試鎖失敗後不會阻塞線程,而是繼續往下執行程式,是以,需要使用if-else語句來判斷是否鎖成功,隻有鎖成功後才能去執行互斥代碼段。而且需要注意的是,因為try_to_lock嘗試鎖失敗後代碼繼續往下執行了,是以該語句不會再次去嘗試鎖。

std::unique_lock所有權的轉移

注意,這裡的轉移指的是std::unique_lock對象間的轉移;std::mutex對象的所有權不需要手動轉移給std::unique_lock , std::unique_lock對象執行個體化後會直接接管std::mutex。

mutex m;
{  
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有權轉移,此時由g3來管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}
           

condition_variable

需要#include<condition_variable>,該頭檔案中包含了條件變量相關的類,其中包括std::condition_variable類。

std::condition_variable類搭配std::mutex類來使用,std::condition_variable對象的作用不是用來管理互斥量的,它的作用是用來同步線程。

類比到std::condition_variable,A、B兩個人約定notify_one為行動号角,A就等着(調用wait(),阻塞),隻要B一調用notify_one,A就開始行動(不再阻塞)。

wait(locker) :wait函數需要傳入一個std::mutex(一般會傳入std::unique_lock對象),即上述的locker。wait函數會自動調用 locker.unlock() 釋放鎖(因為需要釋放鎖,是以要傳入mutex)并阻塞目前線程,本線程釋放鎖使得其他的線程得以繼續競争鎖。一旦目前線程獲得notify(通常是另外某個線程調用 notify_* 喚醒了目前線程),wait() 函數此時再自動調用 locker.lock()上鎖。

cond.notify_one(): 随機喚醒一個等待的線程

cond.notify_all(): 喚醒所有等待的線程

3.異步線程

需要#include<future>

3.1async與future

std::async是一個函數模闆,用來啟動一個異步任務,它傳回一個std::future類模闆對象,future對象起到了占位的作用(記住這點就可以了),占位是什麼意思?就是說該變量現在無值,但将來會有值(好比你擠公交瞧見空了個座位,剛準備坐下去就被旁邊的小夥給攔住了:“這個座位有人了”,你反駁道:”這不是空着嗎?“,小夥:”等會人就來了“),剛執行個體化的future是沒有儲存值的,但在調用std::future對象的get()成員函數時,主線程會被阻塞直到異步線程執行結束,并把傳回結果傳遞給std::future,即通過FutureObject.get()擷取函數傳回值。

相當于你去辦政府辦業務(主線程),把資料交給了前台,前台安排了人員去給你辦理(std::async建立子線程),前台給了你一個單據(std::future對象),說你的業務正在給你辦(子線程正在運作),等段時間你再過來憑這個單據取結果。過了段時間,你去前台取結果(調用get()),但是結果還沒出來(子線程還沒return),你就在前台等着(阻塞),直到你拿到結果(子線程return),你才離開(不再阻塞)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
 double c = a + b;
 Sleep(3000);//假設t1函數是個複雜的計算過程,需要消耗3秒
 return c;
}

int main() 
{
 double a = 2.3;
 double b = 6.7;
 future<double> fu = async(t1, a, b);//建立異步線程線程,并将線程的執行結果用fu占位;
 cout << "正在進行計算" << endl;
 cout << "計算結果馬上就準備好,請您耐心等待" << endl;
 cout << "計算結果:" << fu.get() << endl;//阻塞主線程,直至異步線程return
        //cout << "計算結果:" << fu.get() << endl;//取消該語句注釋後運作會報錯,因為future對象的get()方法隻能調用一次。
 return 0;
}
           

注意:future對象的get()方法隻能調用一次。

3.2shared_future

std::future與std::shard_future的用途都是為了占位,但是兩者有些許差别。std::future的get()成員函數是轉移資料所有權;std::shared_future的get()成員函數是複制資料。 是以: future對象的get()隻能調用一次;無法實作多個線程等待同一個異步線程,一旦其中一個線程擷取了異步線程的傳回值,其他線程就無法再次擷取。 std::shared_future對象的get()可以調用多次;可以實作多個線程等待同一個異步線程,每個線程都可以擷取異步線程的傳回值。

4.原子類型atomic

原子操作指“不可分割的操作”,也就是說這種操作狀态要麼是完成的,要麼是沒完成的,不存在“操作完成了一半”這種狀況。互斥量的加鎖一般是針對一個代碼段,而原子操作針對的一般都是一個變量(操作變量時加鎖防止他人幹擾)。 std::atomic<>是一個模闆類,使用該模闆類執行個體化的對象,提供了一些保證原子性的成員函數來實作共享資料的常用操作。

可以這樣了解: 在以前,定義了一個共享的變量(int i=0),多個線程會用到這個變量,那麼每次操作這個變量時,都需要lock加鎖,操作完畢unlock解鎖,以保證線程之間不會沖突;但是這樣每次加鎖解鎖、加鎖解鎖就顯得很麻煩,那怎麼辦呢? 現在,執行個體化了一個類對象(std::atomic<int> I=0)來代替以前的那個變量(這裡的對象I你就把它看作一個變量,看作對象反而難以了解了),每次操作這個對象時,就不用lock與unlock,這個對象自身就具有原子性(相當于加鎖解鎖操作不用你寫代碼實作,能自動加鎖解鎖了),以保證線程之間不會沖突。

提到std::atomic<>,你腦海裡就想到一點就可以了:std::atomic<>用來定義一個自動加鎖解鎖的共享變量(“定義”“變量”用詞在這裡是不準确的,但是更加貼切它的實際功能),供多個線程通路而不發生沖突。

//原子類型的簡單使用
std::atomic<bool> b(true);
b=false;

std::atomic<int> c(3);
c=4;
           

std::atomic<>對象提供了常見的原子操作(通過調用成員函數實作對資料的原子操作): store是原子寫操作,load是原子讀操作。exchange是于兩個數值進行交換的原子操作。 即使使用了std::atomic<>,也要注意執行的操作是否支援原子性,也就是說,你不要覺得用的是具有原子性的變量(準确說是對象)就可以為所欲為了,你對它進行的運算不支援原子性的話,也不能實作其原子效果。一般針對++,–,+=,-=,&=,|=,^=是支援的,這些原子操作是通過在std::atomic<>對象内部進行運算符重載實作的。