C++ 線程的使用
C++11 之前,C++ 語言沒有對并發(fā)編程提供語言級別的支持,這使得我們在編寫可移植的并發(fā)程序時,存在諸多的不便?,F(xiàn)在 C++11 中增加了線程以及線程相關的類,很方便地支持了并發(fā)編程,使得編寫的多線程程序的可移植性得到了很大的提高。
C++11 中提供的線程類叫做 std::thread,基于這個類創(chuàng)建一個新的線程非常的簡單,只需要提供線程函數(shù)或者函數(shù)對象即可,并且可以同時指定線程函數(shù)的參數(shù)。我們首先來了解一下這個類提供的一些常用 API:
1. 構(gòu)造函數(shù)
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;
構(gòu)造函數(shù)①:默認構(gòu)造函,構(gòu)造一個線程對象,在這個線程中不執(zhí)行任何處理動作
構(gòu)造函數(shù)②:移動構(gòu)造函數(shù),將 other 的線程所有權(quán)轉(zhuǎn)移給新的 thread 對象。之后 other 不再表示執(zhí)行線程。
構(gòu)造函數(shù)③:創(chuàng)建線程對象,并在該線程中執(zhí)行函數(shù) f 中的業(yè)務邏輯,args 是要傳遞給函數(shù) f 的參數(shù)
任務函數(shù) f 的可選類型有很多,具體如下:
普通函數(shù),類成員函數(shù),匿名函數(shù),仿函數(shù)(這些都是可調(diào)用對象類型) 可以是可調(diào)用對象包裝器類型,也可以是使用綁定器綁定之后得到的類型(仿函數(shù))
構(gòu)造函數(shù)④:使用 =delete 顯示刪除拷貝構(gòu)造,不允許線程對象之間的拷貝
2. 公共成員函數(shù)
2.1 get_id()
應用程序啟動之后默認只有一個線程,這個線程一般稱之為主線程或父線程,通過線程類創(chuàng)建出的線程一般稱之為子線程,每個被創(chuàng)建出的線程實例都對應一個線程 ID,這個 ID 是唯一的,可以通過這個 ID 來區(qū)分和識別各個已經(jīng)存在的線程實例,這個獲取線程 ID 的函數(shù)叫做 get_id(),函數(shù)原型如下:
std::thread::id get_id() const noexcept;
示例程序如下:
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void func(int num, string str)
{
for (int i = 0; i < 10; ++i)
{
cout << "子線程: i = " << i << "num: "
<< num << ", str: " << str << endl;
}
}
void func1()
{
for (int i = 0; i < 10; ++i)
{
cout << "子線程: i = " << i << endl;
}
}
int main()
{
cout << "主線程的線程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "線程t 的線程ID: " << t.get_id() << endl;
cout << "線程t1的線程ID: " << t1.get_id() << endl;
}
thread t(func, 520, "i love you");:創(chuàng)建了子線程對象 t,func() 函數(shù)會在這個子線程中運行
func()是一個回調(diào)函數(shù),線程啟動之后就會執(zhí)行這個任務函數(shù),程序猿只需要實現(xiàn)即可func()的參數(shù)是通過 thread 的參數(shù)進行傳遞的,520,i love you都是調(diào)用func()需要的實參線程類的構(gòu)造函數(shù)③ 是一個變參函數(shù),因此無需擔心線程任務函數(shù)的參數(shù)個數(shù)問題 任務函數(shù) func()一般返回值指定為void,因為子線程在調(diào)用這個函數(shù)的時候不會處理其返回值
thread t1(func1);:子線程對象 t1 中的任務函數(shù)func1(),沒有參數(shù),因此在線程構(gòu)造函數(shù)中就無需指定了 通過線程對象調(diào)用get_id()就可以知道這個子線程的線程 ID 了,t.get_id(),t1.get_id()。基于命名空間 this_thread得到當前線程的線程 ID
在上面的示例程序中有一個 bug,在主線程中依次創(chuàng)建出兩個子線程,打印兩個子線程的線程 ID,最后主線程執(zhí)行完畢就退出了(主線程就是執(zhí)行 main () 函數(shù)的那個線程)。默認情況下,主線程銷毀時會將與其關聯(lián)的兩個子線程也一并銷毀,但是這時有可能子線程中的任務還沒有執(zhí)行完畢,最后也就得不到我們想要的結(jié)果了。
當啟動了一個線程(創(chuàng)建了一個 thread 對象)之后,在這個線程結(jié)束的時候(std::terminate ()),我們?nèi)绾稳セ厥站€程所使用的資源呢?thread 庫給我們兩種選擇:
加入式(join()) 分離式(detach())
另外,我們必須要在線程對象銷毀之前在二者之間作出選擇,否則程序運行期間就會有 bug 產(chǎn)生。
2.2 join()
join() 字面意思是連接一個線程,意味著主動地等待線程的終止(線程阻塞)。在某個線程中通過子線程對象調(diào)用 join() 函數(shù),調(diào)用這個函數(shù)的線程被阻塞,但是子線程對象中的任務函數(shù)會繼續(xù)執(zhí)行,當任務執(zhí)行完畢之后 join() 會清理當前子線程中的相關資源然后返回,同時,調(diào)用該函數(shù)的線程解除阻塞繼續(xù)向下執(zhí)行。
再次強調(diào),我們一定要搞清楚這個函數(shù)阻塞的是哪一個線程,函數(shù)在哪個線程中被執(zhí)行,那么函數(shù)就阻塞哪個線程。該函數(shù)的函數(shù)原型如下:
void join();
有了這樣一個線程阻塞函數(shù)之后,就可以解決在上面測試程序中的 bug 了,如果要阻塞主線程的執(zhí)行,只需要在主線程中通過子線程對象調(diào)用這個方法即可,當調(diào)用這個方法的子線程對象中的任務函數(shù)執(zhí)行完畢之后,主線程的阻塞也就隨之解除了。修改之后的示例代碼如下:
int main()
{
cout << "主線程的線程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "線程t 的線程ID: " << t.get_id() << endl;
cout << "線程t1的線程ID: " << t1.get_id() << endl;
t.join();
t1.join();
}
當主線程運行到第八行 t.join();,根據(jù)子線程對象 t 的任務函數(shù) func() 的執(zhí)行情況,主線程會做如下處理:
如果任務函數(shù) func()還沒執(zhí)行完畢,主線程阻塞,直到任務執(zhí)行完畢,主線程解除阻塞,繼續(xù)向下運行如果任務函數(shù) func()已經(jīng)執(zhí)行完畢,主線程不會阻塞,繼續(xù)向下運行
同樣,第 9 行的代碼亦如此。
為了更好的理解
join()的使用,再來給大家舉一個例子,場景如下:
程序中一共有三個線程,其中兩個子線程負責分段下載同一個文件,下載完畢之后,由主線程對這個文件進行下一步處理,那么示例程序就應該這么寫:
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void download1()
{
// 模擬下載, 總共耗時500ms,阻塞線程500ms
this_thread::sleep_for(chrono::milliseconds(500));
cout << "子線程1: " << this_thread::get_id() << ", 找到歷史正文...." << endl;
}
void download2()
{
// 模擬下載, 總共耗時300ms,阻塞線程300ms
this_thread::sleep_for(chrono::milliseconds(300));
cout << "子線程2: " << this_thread::get_id() << ", 找到歷史正文...." << endl;
}
void doSomething()
{
cout << "集齊歷史正文, 呼叫羅賓...." << endl;
cout << "歷史正文解析中...." << endl;
cout << "起航,前往拉夫德爾...." << endl;
cout << "找到OnePiece, 成為海賊王, 哈哈哈!!!" << endl;
cout << "若干年后,草帽全員卒...." << endl;
cout << "大海賊時代再次被開啟...." << endl;
}
int main()
{
thread t1(download1);
thread t2(download2);
// 阻塞主線程,等待所有子線程任務執(zhí)行完畢再繼續(xù)向下執(zhí)行
t1.join();
t2.join();
doSomething();
}
示例程序輸出的結(jié)果:
子線程2: 72540, 找到歷史正文....
子線程1: 79776, 找到歷史正文....
集齊歷史正文, 呼叫羅賓....
歷史正文解析中....
起航,前往拉夫德爾....
找到OnePiece, 成為海賊王, 哈哈哈!!!
若干年后,草帽全員卒....
大海賊時代再次被開啟....
在上面示例程序中最核心的處理是在主線程調(diào)用 doSomething(); 之前在第 35、36行通過子線程對象調(diào)用了 join() 方法,這樣就能夠保證兩個子線程的任務都執(zhí)行完畢了,也就是文件內(nèi)容已經(jīng)全部下載完成,主線程再對文件進行后續(xù)處理,如果子線程的文件沒有下載完畢,主線程就去處理文件,很顯然從邏輯上講是有問題的。
2.3 detach()
detach() 函數(shù)的作用是進行線程分離,分離主線程和創(chuàng)建出的子線程。在線程分離之后,主線程退出也會一并銷毀創(chuàng)建出的所有子線程,在主線程退出之前,它可以脫離主線程繼續(xù)獨立的運行,任務執(zhí)行完畢之后,這個子線程會自動釋放自己占用的系統(tǒng)資源。(其實就是孩子翅膀硬了,和家里斷絕關系,自己外出闖蕩了,如果家里被誅九族還是會受牽連)。該函數(shù)函數(shù)原型如下:
void detach();
線程分離函數(shù)沒有參數(shù)也沒有返回值,只需要在線程成功之后,通過線程對象調(diào)用該函數(shù)即可,繼續(xù)將上面的測試程序修改一下:
int main()
{
cout << "主線程的線程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "線程t 的線程ID: " << t.get_id() << endl;
cout << "線程t1的線程ID: " << t1.get_id() << endl;
t.detach();
t1.detach();
// 讓主線程休眠, 等待子線程執(zhí)行完畢
this_thread::sleep_for(chrono::seconds(5));
}
注意事項:線程分離函數(shù)
detach ()不會阻塞線程,子線程和主線程分離之后,在主線程中就不能再對這個子線程做任何控制了,比如:通過join ()阻塞主線程等待子線程中的任務執(zhí)行完畢,或者調(diào)用get_id ()獲取子線程的線程 ID。有利就有弊,魚和熊掌不可兼得,建議使用join ()。
2.5 joinable()
joinable() 函數(shù)用于判斷主線程和子線程是否處理關聯(lián)(連接)狀態(tài),一般情況下,二者之間的關系處于關聯(lián)狀態(tài),該函數(shù)返回一個布爾類型:
返回值為 true:主線程和子線程之間有關聯(lián)(連接)關系 返回值為 false:主線程和子線程之間沒有關聯(lián)(連接)關系
bool joinable() const noexcept;
示例代碼如下:
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void foo()
{
this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
thread t;
cout << "before starting, joinable: " << t.joinable() << endl;
t = thread(foo);
cout << "after starting, joinable: " << t.joinable() << endl;
t.join();
cout << "after joining, joinable: " << t.joinable() << endl;
thread t1(foo);
cout << "after starting, joinable: " << t1.joinable() << endl;
t1.detach();
cout << "after detaching, joinable: " << t1.joinable() << endl;
}
示例代碼打印的結(jié)果如下:
before starting, joinable: 0
after starting, joinable: 1
after joining, joinable: 0
after starting, joinable: 1
after detaching, joinable: 0
基于示例代碼打印的結(jié)果可以得到以下結(jié)論:
在創(chuàng)建的子線程對象的時候,如果沒有指定任務函數(shù),那么子線程不會啟動,主線程和這個子線程也不會進行連接 在創(chuàng)建的子線程對象的時候,如果指定了任務函數(shù),子線程啟動并執(zhí)行任務,主線程和這個子線程自動連接成功 子線程調(diào)用了 detach()函數(shù)之后,父子線程分離,同時二者的連接斷開,調(diào)用joinable()返回false在子線程調(diào)用了 join()函數(shù),子線程中的任務函數(shù)繼續(xù)執(zhí)行,直到任務處理完畢,這時join()會清理(回收)當前子線程的相關資源,所以這個子線程和主線程的連接也就斷開了,因此,調(diào)用join()之后再調(diào)用joinable()會返回false。
2.6 operator=
線程中的資源是不能被復制的,因此通過 = 操作符進行賦值操作最終并不會得到兩個完全相同的對象。
// move (1)
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete;
通過以上 = 操作符的重載聲明可以得知:
如果 other 是一個右值,會進行資源所有權(quán)的轉(zhuǎn)移 如果 other 不是右值,禁止拷貝,該函數(shù)被顯示刪除(=delete),不可用
3. 靜態(tài)函數(shù)
thread 線程類還提供了一個靜態(tài)方法,用于獲取當前計算機的 CPU 核心數(shù),根據(jù)這個結(jié)果在程序中創(chuàng)建出數(shù)量相等的線程,每個線程獨自占有一個 CPU 核心,這些線程就不用分時復用 CPU 時間片,此時程序的并發(fā)效率是最高的。
static unsigned hardware_concurrency() noexcept;
示例代碼如下:
#include <iostream>
#include <thread>
using namespace std;
int main()
{
int num = thread::hardware_concurrency();
cout << "CPU number: " << num << endl;
}
4. C 線程庫
C 語言提供的線程庫不論在 window 還是 Linux 操作系統(tǒng)中都是可以使用的,看明白了這些 C 語言中的線程函數(shù)之后會發(fā)現(xiàn)它和上面的 C++ 線程類使用很類似(其實就是基于面向?qū)ο蟮乃枷脒M行了封裝),但 C++ 的線程類用起來更簡單一些,鏈接奉上,感興趣的可以一看。
文章鏈接:https://subingwen.com/cpp/thread/
