主要參考: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ù)測雇卷。
為了避免競爭,我們需要在線程進(jìn)入關(guān)鍵段(critical section)的時候阻止并行颠猴。為此关划,我們引入互斥鎖。
互斥鎖
在我們進(jìn)入一個關(guān)鍵段的時候翘瓮,線程檢查互斥鎖是否是鎖住的:
- 如果鎖住贮折,線程阻塞
- 如果沒有,則進(jìn)入關(guān)鍵段
std::mutex
有兩個成員函數(shù)lock
和unlock
资盅。
然而调榄,對互斥鎖使用不當(dāng)可能導(dǎo)致死鎖(deadlock):
- 原因1:忘記
unlock
一個mutex
解決方案:使用scoped locklocak_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ù)君珠。
#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é)果松嘶。