一、簡介
抽象損失:對于實現某個功能時,可以使用高級工具,也可以直接使用底層工具。這兩種方式運行的開銷差異稱為抽象損失。
二、線程管控
2.1 線程的基本控制
1. 創建線程
線程相關的管理函數和類在頭文件:
#include <thread>
創建一個線程使用如下方法:
std::thread t(callable);
callable
:線程函數,可以是任意的可調用對象- 線程對象創建后會立即啟動線程運行
2. 控制線程的結束
線程啟動后,必須顯式指定線程結束的方式:
阻塞等待其結束(匯合),使用如下方法:
t.join();
- 只要調用了
join()
,主線程會阻塞等待該線程執行結束,join()
執行結束后,隸屬于該線程的任何存儲空間都會被清除,且線程對象不再關聯到結束的線程 - 成員函數
t.joinable()
返回線程對象t
是否關聯到某個線程,當join()
執行結束后t.joinable()
會返回false
- 只有關聯到某個線程的對象才能調用
join()
讓線程后臺運行(分離),使用如下方法:
t.detach();
- 線程對象必須關聯某個線程(
joinable()
為true
時)才能調用detach
,調用后線程的歸屬權和控制權都轉移給C++運行時庫,它獨立于主線程繼續運行,直至線程函數運行結束,且C++運行時庫會保證線程退出后與之關聯的資源都被正確回收 - 被
detach
的線程不再關聯實際的執行線程(joinable()
會返回false
),所以不可再調用join()
注意:如果線程對象銷毀時都沒有指定結束方式,則std::thread
的析構函數會調用std::terminate()
終止整個程序
3. 異常時保證線程正常結束
創建線程對象后,如果在指定線程結束方式前,因為執行其他代碼造成了異常,可能會導致指定線程結束方式的代碼被略過,從而導致程序終止。即:
void func() {try {// 線程 t 去執行 do_something() 函數std::thread t(do_something);// 此時如果發生異常等,下面的join可能無法被調用,退出func時程序會被中斷do_other_thing();} catch (...) {throw;}t.join();return;
}
解決方法:
- 一種解決方法是保證在程序的所有執行路徑都會指定線程結束方式
void func() {try {std::thread t(do_something);do_other_thing();} catch (...) {// 保證一定會指定線程結束方式t.join();throw;}t.join();return; }
- 使用RAII方法,利用對象管理線程,在構造函數中創建線程,在析構函數中指定線程的結束方式
class RaiiThreadGuard {public:explicit RaiiThreadGuard(std::thread &t) : t_(t) {} ~RaiiThreadGuard() {// 類對象離開作用域時一定會調用析構,保證一定會指定線程對象的結束方式t_.join();}// 將拷貝構造和拷貝賦值都定義為刪除的,避免編譯器優化造成重復調用析構RaiiThreadGuard(const RaiiThreadGuard &) = delete;RaiiThreadGuard& operator=(const RaiiThreadGuard &) = delete;private:std::thread& t_; };// main 函數中通過如下方式使用 int main() {// 創建線程std::thread t(HelloFunction);// 使用 RAII 保證線程對象一定會調用 joinRaiiThreadGuard thread_guard(t);// 即使后續再執行其他代碼造成退出作用域,編譯器會保證執行 thread_guard 的析構指定線程的結束方式return 0; }
2.2 向線程函數傳遞參數
線程函數所需要的參數可以直接緊跟在std::thread
的線程函數實參后:
- 線程具有內部存儲空間,線程函數的實參會先使用拷貝構造函數,將
std::thread
的實參復制到創建的線程;在創建好的線程中,新復制出來的實參被當成臨時量,以右值形式傳遞給新線程中的線程函數 - 根據參數的傳遞過程,如果線程函數包含非const引用形參,為避免在線程內執行時收到右值,需要通過
std::ref
在std::thread
傳遞實參(與std::bind
函數的使用相同) - 類的非靜態成員函數第一個參數是指向對象的隱式
this
指針,如果想在線程中執行某個成員函數,需要將對象地址傳遞給成員函數的隱式this
指針
class TestClass {
public:/** @brief 默認構造函數 */TestClass(){std::cout << "TestClass default constructor." << std::endl;std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;}/** @brief 拷貝構造函數 */TestClass(const TestClass& ohter) {std::cout << "TestClass copy constructor." << std::endl;std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;}/** @brief 移動構造函數 */TestClass(TestClass&& ohter) {std::cout << "TestClass move constructor." << std::endl;std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;}/** @brief 類的內部函數 */void InnerFunction() {std::cout << "TestClass Inner Function." << std::endl;std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;}
};void func(int num, std::string &str, TestClass obj) {str += std::to_string(num);
}// main 函數
std::string str("The num: ");
TestClass test_class;
// 參數直接作為 std::thread 的后續參數傳入
// 1. 對象會先調用拷貝構造復制到線程,再通過std::move()以右值復制給線程內的函數形參
// 2. 線程函數的引用形參要通過 std::ref() 傳遞
std::thread t(func, 3, std::ref(str), test_class);
t.join();
std::cout << str << std::endl;// 在線程中運行類的成員函數,需要將對象的地址傳遞給成員函數的隱式this指針
std::thread t2(&TestClass::InnerFunction, &test_class);
t2.join();/* 輸出
TestClass default constructor.
std::thread::id: 140737348195264
TestClass copy constructor.
std::thread::id: 140737348195264
TestClass move constructor.
std::thread::id: 140737348179520
The num: 3
TestClass Inner Function.
std::thread::id: 140737348179520
*/
2.3 轉移線程歸屬權
在某些情況下,如想要指定特定的函數等待線程結束,可能需要轉移線程的歸屬權。
std::thread
與std::unique
類似,由于獨占資源,其對象不能被復制,只支持移動:
std::thread t1(some_function);
std::thread t2 = std::move(t1);
2.4 運行時確定線程數量
標準庫的靜態函數std::thread::hardware_concurrency()
函數返回程序在執行時可以真正并發的線程數量:
- 若信息無法獲取,返回0
- 否則返回支持并發的線程數
2.5 標識不同線程
每個線程都有一個唯一的ID,該ID是一個std::thread::id
類型的變量:
- 可以使用線程對象的
std::thread::get_id()
函數返回線程對象的ID - 在程序中可以使用
std::this_thread::get_id()
函數獲取運行當前程序的線程ID - 默認構造創建的
std::thread::id
類型變量表示線程不存在
std::thread::joinable()
函數會利用線程對象的ID確定返回值,即:
- 若
this.get_id() != std::thread::id()
則返回 true(判斷當前線程ID和默認構造的線程ID類型變量是否相同) - 否則返回false(表示線程對象沒有關聯到任何線程,線程不存在)