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