C++11 之前,C++ 語言沒有對并發程式設計提供語言級别的支援,這使得我們在編寫可移植的并發程式時,存在諸多的不便。現在 C++11 中增加了線程以及線程相關的類,很友善地支援了并發程式設計,使得編寫的多線程程式的可移植性得到了很大的提高。
C++11 中提供的線程類叫做
std::thread
,基于這個類建立一個新的線程非常的簡單,隻需要提供線程函數或者函數對象即可,并且可以同時指定線程函數的參數。我們首先來了解一下這個類提供的一些常用 API:
1. 構造函數
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;
構造函數①:預設構造函,構造一個線程對象,在這個線程中不執行任何處理動作
構造函數②:移動構造函數,将 other 的線程所有權轉移給新的 thread 對象。之後 other 不再表示執行線程。
構造函數③:建立線程對象,并在該線程中執行函數 f 中的業務邏輯,args 是要傳遞給函數 f 的參數
任務函數 f 的可選類型有很多,具體如下:
- 普通函數,類成員函數,匿名函數,仿函數(這些都是可調用對象類型)
- 可以是可調用對象包裝器類型,也可以是使用綁定器綁定之後得到的類型(仿函數)
構造函數④:使用 =delete 顯示删除拷貝構造,不允許線程對象之間的拷貝
2. 公共成員函數
2.1 get_id()
應用程式啟動之後預設隻有一個線程,這個線程一般稱之為主線程或父線程,通過線程類建立出的線程一般稱之為子線程,每個被建立出的線程執行個體都對應一個線程 ID,這個 ID 是唯一的,可以通過這個 ID 來區分和識别各個已經存在的線程執行個體,這個擷取線程 ID 的函數叫做
get_id()
,函數原型如下:
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;
}
-
;:建立了子線程對象 t,func() 函數會在這個子線程中運作thread t(func, 520, "i love you")
-
是一個回調函數,線程啟動之後就會執行這個任務函數,程式猿隻需要實作即可func()
-
的參數是通過 thread 的參數進行傳遞的,func()
都是調用 520,i love you
需要的實參func()
- 線程類的構造函數③ 是一個變參函數,是以無需擔心線程任務函數的參數個數問題
- 任務函數
一般傳回值指定為 func()
,因為子線程在調用這個函數的時候不會處理其傳回值void
-
;:子線程對象 t1 中的任務函數thread t1(func1)
,沒有參數,是以線上程構造函數中就無需指定了 通過線程對象調用 func1()
就可以知道這個子線程的線程 ID 了,get_id()
,t.get_id()
。t1.get_id()
- 基于命名空間
得到目前線程的線程 IDthis_thread
在上面的示例程式中有一個 bug,在主線程中依次建立出兩個子線程,列印兩個子線程的線程 ID,最後主線程執行完畢就退出了(主線程就是執行
main ()
函數的那個線程)。預設情況下,主線程銷毀時會将與其關聯的兩個子線程也一并銷毀,但是這時有可能子線程中的任務還沒有執行完畢,最後也就得不到我們想要的結果了。
當啟動了一個線程(建立了一個 thread 對象)之後,在這個線程結束的時候(
std::terminate ()
),我們如何去回收線程所使用的資源呢?thread 庫給我們兩種選擇:
- 加入式(join())
- 分離式(detach())
另外,我們必須要線上程對象銷毀之前在二者之間作出選擇,否則程式運作期間就會有 bug 産生。
2.2 join()
join()
字面意思是連接配接一個線程,意味着主動地等待線程的終止(線程阻塞)。在某個線程中通過子線程對象調用
join()
函數,調用這個函數的線程被阻塞,但是子線程對象中的任務函數會繼續執行,當任務執行完畢之後
join()
會清理目前子線程中的相關資源然後傳回,同時,調用該函數的線程解除阻塞繼續向下執行。
再次強調,我們一定要搞清楚這個函數阻塞的是哪一個線程,函數在哪個線程中被執行,那麼函數就阻塞哪個線程。該函數的函數原型如下:
void join();
有了這樣一個線程阻塞函數之後,就可以解決在上面測試程式中的 bug 了,如果要阻塞主線程的執行,隻需要在主線程中通過子線程對象調用這個方法即可,當調用這個方法的子線程對象中的任務函數執行完畢之後,主線程的阻塞也就随之解除了。修改之後的示例代碼如下:
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()
;,根據子線程對象 t 的任務函數
func()
的執行情況,主線程會做如下處理:
- 如果任務函數
還沒執行完畢,主線程阻塞,直到任務執行完畢,主線程解除阻塞,繼續向下運作func()
- 如果任務函數
已經執行完畢,主線程不會阻塞,繼續向下運作func()
同樣,第 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);
// 阻塞主線程,等待所有子線程任務執行完畢再繼續向下執行
t1.join();
t2.join();
doSomething();
}
示例程式輸出的結果:
子線程2: 72540, 找到曆史正文....
子線程1: 79776, 找到曆史正文....
集齊曆史正文, 呼叫羅賓....
曆史正文解析中....
起航,前往拉夫德爾....
找到OnePiece, 成為海賊王, 哈哈哈!!!
若幹年後,草帽全員卒....
大海賊時代再次被開啟....
在上面示例程式中最核心的處理是在主線程調用
doSomething()
; 之前在第 35、36行通過子線程對象調用了
join()
方法,這樣就能夠保證兩個子線程的任務都執行完畢了,也就是檔案内容已經全部下載下傳完成,主線程再對檔案進行後續處理,如果子線程的檔案沒有下載下傳完畢,主線程就去處理檔案,很顯然從邏輯上講是有問題的。
2.3 detach()
detach()
函數的作用是進行線程分離,分離主線程和建立出的子線程。線上程分離之後,主線程退出也會一并銷毀建立出的所有子線程,在主線程退出之前,它可以脫離主線程繼續獨立的運作,任務執行完畢之後,這個子線程會自動釋放自己占用的系統資源。(其實就是孩子翅膀硬了,和家裡斷絕關系,自己外出闖蕩了,如果家裡被誅九族還是會受牽連)。該函數函數原型如下:
void detach();
線程分離函數沒有參數也沒有傳回值,隻需要線上程成功之後,通過線程對象調用該函數即可,繼續将上面的測試程式修改一下:
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();
// 讓主線程休眠, 等待子線程執行完畢
this_thread::sleep_for(chrono::seconds(5));
}
注意事項:線程分離函數 不會阻塞線程,子線程和主線程分離之後,在主線程中就不能再對這個子線程做任何控制了,比如:通過
detach ()
阻塞主線程等待子線程中的任務執行完畢,或者調用
join ()
擷取子線程的線程 ID。有利就有弊,魚和熊掌不可兼得,建議使用
get_id ()
。
join ()
2.5 joinable()
joinable()
函數用于判斷主線程和子線程是否處理關聯(連接配接)狀态,一般情況下,二者之間的關系處于關聯狀态,該函數傳回一個布爾類型:
- 傳回值為 true:主線程和子線程之間有關聯(連接配接)關系
- 傳回值為 false:主線程和子線程之間沒有關聯(連接配接)關系
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;
}
示例代碼列印的結果如下:
before starting, joinable: 0
after starting, joinable: 1
after joining, joinable: 0
after starting, joinable: 1
after detaching, joinable: 0
基于示例代碼列印的結果可以得到以下結論:
- 在建立的子線程對象的時候,如果沒有指定任務函數,那麼子線程不會啟動,主線程和這個子線程也不會進行連接配接
- 在建立的子線程對象的時候,如果指定了任務函數,子線程啟動并執行任務,主線程和這個子線程自動連接配接成功
- 子線程調用了
函數之後,父子線程分離,同時二者的連接配接斷開,調用detach()
傳回falsejoinable()
- 在子線程調用了
函數,子線程中的任務函數繼續執行,直到任務處理完畢,這時join()
會清理(回收)目前子線程的相關資源,是以這個子線程和主線程的連接配接也就斷開了,是以,調用join()
之後再調用join()
會傳回false。joinable()
2.6 operator=
線程中的資源是不能被複制的,是以通過 = 操作符進行指派操作最終并不會得到兩個完全相同的對象。
// move (1)
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete;
通過以上 = 操作符的重載聲明可以得知:
- 如果 other 是一個右值,會進行資源所有權的轉移
- 如果 other 不是右值,禁止拷貝,該函數被顯示删除(=delete),不可用
3. 靜态函數
thread 線程類還提供了一個靜态方法,用于擷取目前計算機的 CPU 核心數,根據這個結果在程式中建立出數量相等的線程,每個線程獨自占有一個 CPU 核心,這些線程就不用分時複用 CPU 時間片,此時程式的并發效率是最高的。
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 作業系統中都是可以使用的,看明白了這些 C 語言中的線程函數之後會發現它和上面的 C++ 線程類使用很類似(其實就是基于面向對象的思想進行了封裝),但 C++ 的線程類用起來更簡單一些,連結奉上,感興趣的可以一看。