C++ 多線程入門

主要參考:Advanced Operating Systems-Multi-threading in C++ from Giuseppe Massari and Federico Terraneo

介紹

多任務(wù)處理器允許我們同時運(yùn)行多個任務(wù)。操作系統(tǒng)會為不同的進(jìn)程分配獨(dú)立的地址空間。
多線程允許一個進(jìn)程在共享的地址空間里執(zhí)行多個任務(wù)固歪。

線程

一個線程是一個輕量的任務(wù)。
每個線程擁有獨(dú)立的棧和context岁忘。


多線程

取決于具體的實(shí)現(xiàn)伶选,線程至核心的安排由OS或者language runtime來負(fù)責(zé)。

C++對線程的支持

新建線程

void myThread() {
    for (;;) {
        std::cout << "world" << std::endl; 
    }
}
int main() {
    std::thread t(myThread);
    for(;;) {
        std::cout << "hello " << std::endl;
    }
}

std::thread的構(gòu)造函數(shù)可以以一個可調(diào)用對象和一系列參數(shù)為參數(shù)來啟動一個線程執(zhí)行這個可調(diào)用對象牵舱。
除了上面例子里的函數(shù)(myThread)外,仿函數(shù)(functor)也是線程常用的可調(diào)用對象缺虐。
仿函數(shù)是一個定義和實(shí)現(xiàn)了operator()成員函數(shù)的類芜壁。與普通的函數(shù)相比,可以賦予其一些類的性質(zhì),如繼承慧妄、多態(tài)等顷牌。
std::thread::join()等待線程結(jié)束,調(diào)用后thread變?yōu)閡njoinable塞淹。
std::thread::detach()將線程與thread對象脫離窟蓝,調(diào)用后thread變?yōu)閡njoinalbe。
bool std::thread::joinable()返回線程是否可加入饱普。

同步

static int sharedVariable = 0;
void myThread() {
    for (int i=0; i<1000000; i++) sharedVariable++;
}
int main() {
    std::thread t(myThread);
    for (int i=0; i<1000000; i++) sharedVariable--;
    t.join();
    std::cout<<"sharedVariable="<<sharedVariable<<std::endl;
}

上面的程序會遇到數(shù)據(jù)競爭的問題运挫,因?yàn)?code>++和--都不是元操作(atomic operation),實(shí)際上我們需要拿到數(shù)據(jù)费彼、遞增/遞減滑臊、放回數(shù)據(jù)三步,而兩個線程可能會在對方?jīng)]有完成三步的時候就插入箍铲,導(dǎo)致結(jié)果不可預(yù)測雇卷。

image.png

為了避免競爭,我們需要在線程進(jìn)入關(guān)鍵段(critical section)的時候阻止并行颠猴。為此关划,我們引入互斥鎖。

互斥鎖

在我們進(jìn)入一個關(guān)鍵段的時候翘瓮,線程檢查互斥鎖是否是鎖住的:

  • 如果鎖住贮折,線程阻塞
  • 如果沒有,則進(jìn)入關(guān)鍵段

std::mutex有兩個成員函數(shù)lockunlock资盅。
然而调榄,對互斥鎖使用不當(dāng)可能導(dǎo)致死鎖(deadlock):

  • 原因1:忘記unlock一個mutex
    解決方案:使用scoped lock locak_guard<mutex>,會在析構(gòu)的時候自動釋放互斥鎖呵扛。
    std::mutex myMutex;
    void muFunctions(int value) {
        {
            std::lock_guard<std::mutex> lck(myMutex);
            //...
        }
    } 
    
  • 原因2:同一個互斥鎖被嵌套的函數(shù)使用
    解決方案:使用recursive_mutex每庆,允許同一個線程多次使用同一個互斥鎖。
    std::recursive_mutex myMutex;
    void func2() {
        std::lock_guard<recursive_mutex> lck(myMutex);
        //do some thing
    }
    void func1() {
        std::lock_guard<recursive_mutex> lck(myMutex);
        //do some thing
        func2();
    }
    
  • 原因3:多個線程用不同的順序調(diào)用互斥鎖
    解決方案:使用lock(..)函數(shù)取代mutex::lock()成員函數(shù)今穿,該函數(shù)會自動判斷上鎖的順序缤灵。
    mutex myMutex1, myMutex2;
    void func2() {
        lock(myMutex1, myMutex2);
        //do something
        myMutex1.unlock();
        myMutex2.unlock();
    }
    void func1() {
        lock(myMutex2, myMutex1);
        //do something
        myMutex1.unlock();
        myMutex2.unlock();
    }
    

條件變量

有的時候,線程之間有依賴關(guān)系蓝晒,這種時候需要一些線程等待其他線程完成特定的操作腮出。
std::condition_variable條件變量,有三個成員函數(shù):

  • wait(unique_lock<mutex> &):阻塞當(dāng)前線程芝薇,直到另一個線程將其喚醒胚嘲。在wait(...)的過程中,互斥鎖是解鎖的狀態(tài)洛二。
  • notify_one():喚醒一個等待線程馋劈。
  • notify_all():喚醒所有等待線程立倍。
using namespace std;
string shared;
mutex myMutex;
condition_variable myCv;

void myThread() {
    unique_lock<mutex> lck(myMutex);
    while (shared.empty()) myCv.wait(lck);
    cout << shared << endl;
}

int main() {
    thread t(myThread);
    string s;
    cin >> s;
    {
        unique_lock<mutex> lck(myMutex);
        shared = s;
        myCv.notify_one();
    }
    t.join();
}

另外有一個比較小的點(diǎn):為什么wait()通常放在循環(huán)中調(diào)用,是為了保證condition_variable被喚醒的時候條件仍然會被判斷一次侣滩。

設(shè)計模式

Producer/Consumer

一個消費(fèi)者線程需要生產(chǎn)者線程提供數(shù)據(jù)。
為了讓兩個線程的操作解耦变擒,我們設(shè)計一個隊(duì)列用來緩存數(shù)據(jù)君珠。


image.png
#include <list>
#include <mutex>
#include <condition_variable>

template<typename T>
class SynchronizedQueue {
public:
    SynchronizedQueue();
    void put(const T&);
    T get();
private:
    SynchronizedQueue(const SynchronizedQueue&);
    SynchronizedQueue &operator=(const SynchronizedQueue&);
    std::list<T> queue;
    std::mutex myMutex;
    std::condition_variable myCv;
};

template<typename T>
void SynchronizedQueue<T>::put (const T& data) {
    std::unique_lock<std::mutex> lck(myMutex);
    queue.push_backdata();
    myCv.notify_one();
}

template<typename T>
T SynchronizedQueue<T>::get() {
    std::unique_lock<std::mutex> lck(myMutex);
    while(queue.empty())
        myCv.wait(lck);
    T result = queue.front();
    queue.pop_front();
    return result;
}

Active Object

目標(biāo)是實(shí)例化一個任務(wù)對象。
通常來說娇斑,其他線程無法通過顯式的方法與一個線程函數(shù)通信策添,數(shù)據(jù)常常是通過全局變量在線程之間交流。
這種設(shè)計模式讓我們能夠在一個對象里封裝一個線程毫缆,從而獲得一個擁有可調(diào)用方法的線程唯竹。
設(shè)計一個類,擁有一個thread成員變量和一個run()成員函數(shù)苦丁。

//active_object.hpp
#include <atomic>
#include <thread>

class ActiveObject {
public:
    ActiveObject();
    ~ActiveObject();
private:
    virtual void run();
    ActiveObject(const ActiveObject&);
    ActiveObject& operator=(const ActiveObject&);
protected:
    std::thread t;
    std::atomic<bool> quit;
};

//active_object.cpp
#include "active_object.hpp"
#include <functional>

ActiveObject::ActiveObject() :
    t(std::bind(&ActiveObject::run, this)), quit(false) {}

void ActiveObject::run() {
    while(!quit.load()) {
        // do something
    }
}

ActiveObject::~ActiveObject() {
    if(quit.load()) return;
    quit.store(true);
    t.join();
}

其中std::bind可以用于基于函數(shù)和部分/全部參數(shù)構(gòu)建一個新的可調(diào)用對象浸颓。

Reactor

Reactor的目標(biāo)在于讓任務(wù)的產(chǎn)生和執(zhí)行解耦。會有一個任務(wù)隊(duì)列旺拉,同時有一個執(zhí)行線程負(fù)責(zé)一次執(zhí)行隊(duì)列里的任務(wù)(FIFO产上,當(dāng)然也可以設(shè)計其他的執(zhí)行順序)。Reactor本身可以繼承自Active object蛾狗,同時維護(hù)一個Synchronized Queue作為成員變量晋涣。
這樣我們擁有了一個線程,它能夠在執(zhí)行的過程中不斷地接受新的任務(wù)沉桌,同時避免了線程頻繁的構(gòu)建和析構(gòu)所浪費(fèi)的資源谢鹊。

ThreadPool

Reactor的局限在于任務(wù)是順序完成的,而線程池Thread Pool則允許我們讓多個線程監(jiān)聽同一個任務(wù)隊(duì)列留凭。
一個比較不錯的實(shí)現(xiàn)可以參考這里:https://blog.csdn.net/MOU_IT/article/details/88712090
通常來說佃扼,一個線程池需要有以下幾個元素:

  • 管理器(創(chuàng)建線程、啟動/停止/添加任務(wù))
  • 任務(wù)隊(duì)列
  • 任務(wù)接口(任務(wù)抽象)
  • 工作線程

其他概念

還有一些其他的與多線程息息相關(guān)的概念:

atomic原子類型

常見的比如用std::atomic<bool>或者std::atomic_bool取代bool類型變量冰抢。
原子類型主要涉及以下幾個問題(參考):

tearing: a read or write involves multiple bus cycles, and a thread switch occurs in the middle of the operation; this can produce incorrect values.
cache coherence: a write from one thread updates its processor's cache, but does not update global memory; a read from a different thread reads global memory, and doesn't see the updated value in the other processor's cache.
compiler optimization: the compiler shuffles the order of reads and writes under the assumption that the values are not accessed from another thread, resulting in chaos.
Using std::atomic<bool> ensures that all three of these issues are managed correctly. Not using std::atomic<bool> leaves you guessing, with, at best, non-portable code.

future和promise

在線程池里常常會用到異步讀取線程運(yùn)行的結(jié)果松嘶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挎扰,隨后出現(xiàn)的幾起案子翠订,更是在濱河造成了極大的恐慌,老刑警劉巖遵倦,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尽超,死亡現(xiàn)場離奇詭異,居然都是意外死亡梧躺,警方通過查閱死者的電腦和手機(jī)似谁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門傲绣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人巩踏,你說我怎么就攤上這事秃诵。” “怎么了塞琼?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵菠净,是天一觀的道長。 經(jīng)常有香客問我彪杉,道長毅往,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任派近,我火速辦了婚禮攀唯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘渴丸。我一直安慰自己侯嘀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布谱轨。 她就那樣靜靜地躺著残拐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碟嘴。 梳的紋絲不亂的頭發(fā)上溪食,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音娜扇,去河邊找鬼错沃。 笑死,一個胖子當(dāng)著我的面吹牛雀瓢,可吹牛的內(nèi)容都是我干的枢析。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼刃麸,長吁一口氣:“原來是場噩夢啊……” “哼醒叁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起泊业,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤把沼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吁伺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饮睬,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年篮奄,在試婚紗的時候發(fā)現(xiàn)自己被綠了捆愁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片割去。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖昼丑,靈堂內(nèi)的尸體忽然破棺而出呻逆,到底是詐尸還是另有隱情,我是刑警寧澤菩帝,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布页慷,位于F島的核電站,受9級特大地震影響胁附,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滓彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一控妻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揭绑,春花似錦弓候、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至邦蜜,卻和暖如春依鸥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背悼沈。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工贱迟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人絮供。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓衣吠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親壤靶。 傳聞我的和親對象是個殘疾皇子缚俏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 最近是恰好寫了一些c++11多線程有關(guān)的東西,就寫一下筆記留著以后自己忘記回來看吧贮乳,也不是專門寫給讀者看的忧换,我就想...
    編程小世界閱讀 2,483評論 1 2
  • 介紹:什么是線程,線程的優(yōu)點(diǎn)是什么 線程在Unix系統(tǒng)下向拆,通常被稱為輕量級的進(jìn)程包雀,線程雖然不是進(jìn)程,但卻可以看作是...
    未來已來_1cab閱讀 2,151評論 0 3
  • 本文根據(jù)眾多互聯(lián)網(wǎng)博客內(nèi)容整理后形成亲铡,引用內(nèi)容的版權(quán)歸原始作者所有才写,僅限于學(xué)習(xí)研究使用葡兑,不得用于任何商業(yè)用途。 互...
    深紅的眼眸閱讀 1,092評論 0 0
  • 互斥量 用于線程同步赞草,保證多線程訪問共享數(shù)據(jù)的正確性 基本類型 std::mutex:獨(dú)占的互斥量讹堤,不能遞歸使用 ...
    JasonLiThirty閱讀 555評論 0 1
  • 接著上節(jié) atomic,本節(jié)主要介紹condition_varible的內(nèi)容厨疙,練習(xí)代碼地址洲守。本文參考http://...
    jorion閱讀 8,464評論 0 7