C++ 多線程入門

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

介紹

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

線程

一個(gè)線程是一個(gè)輕量的任務(wù)。
每個(gè)線程擁有獨(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ù)可以以一個(gè)可調(diào)用對象和一系列參數(shù)為參數(shù)來啟動一個(gè)線程執(zhí)行這個(gè)可調(diào)用對象。
除了上面例子里的函數(shù)(myThread)外,仿函數(shù)(functor)也是線程常用的可調(diào)用對象。
仿函數(shù)是一個(gè)定義和實(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ù)、遞增/遞減、放回?cái)?shù)據(jù)三步,而兩個(gè)線程可能會在對方?jīng)]有完成三步的時(shí)候就插入,導(dǎo)致結(jié)果不可預(yù)測。

image.png

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

互斥鎖

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

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

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

  • 原因1:忘記unlock一個(gè)mutex
    解決方案:使用scoped lock locak_guard<mutex>,會在析構(gòu)的時(shí)候自動釋放互斥鎖。
    std::mutex myMutex;
    void muFunctions(int value) {
        {
            std::lock_guard<std::mutex> lck(myMutex);
            //...
        }
    } 
    
  • 原因2:同一個(gè)互斥鎖被嵌套的函數(shù)使用
    解決方案:使用recursive_mutex,允許同一個(gè)線程多次使用同一個(gè)互斥鎖。
    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:多個(gè)線程用不同的順序調(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();
    }
    

條件變量

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

  • wait(unique_lock<mutex> &):阻塞當(dāng)前線程,直到另一個(gè)線程將其喚醒。在wait(...)的過程中,互斥鎖是解鎖的狀態(tài)。
  • notify_one():喚醒一個(gè)等待線程。
  • 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();
}

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

設(shè)計(jì)模式

Producer/Consumer

一個(gè)消費(fèi)者線程需要生產(chǎn)者線程提供數(shù)據(jù)。
為了讓兩個(gè)線程的操作解耦,我們設(shè)計(jì)一個(gè)隊(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í)例化一個(gè)任務(wù)對象。
通常來說,其他線程無法通過顯式的方法與一個(gè)線程函數(shù)通信,數(shù)據(jù)常常是通過全局變量在線程之間交流。
這種設(shè)計(jì)模式讓我們能夠在一個(gè)對象里封裝一個(gè)線程,從而獲得一個(gè)擁有可調(diào)用方法的線程。
設(shè)計(jì)一個(gè)類,擁有一個(gè)thread成員變量和一個(gè)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)建一個(gè)新的可調(diào)用對象。

Reactor

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

ThreadPool

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

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

其他概念

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

atomic原子類型

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

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)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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