Wiki
std::thread
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
std::thread 是 C++11 提供的標準執行緒管理類,定義在 <thread> 中。
這一節不只要學會“開一個執行緒”,更重要的是理解:
std::thread物件和實際執行執行緒是什麼關係;- 建構函式裡第一個引數和後續引數分別表示什麼;
- 引數預設是如何儲存的;
join()、detach()、joinable()分別改變了什麼;- 為什麼執行緒物件的解構函式可能直接終止程式;
- 為什麼
std::thread不能複製,但可以移動。
1.std::thread 物件不是“執行緒本身”
可以把 std::thread 理解成一個 管理執行緒執行實體的 C++ 物件。
一個 std::thread 物件可能處於兩種重要狀態:
未关联任何执行线程
或
关联着一个可 join 的执行线程
這個狀態可以通過:
thread.joinable()
查詢。
例如:
#include <iostream>
#include <thread>
void work()
{
std::cout << "Work Done!" << std::endl;
}
int main()
{
std::thread t1;
std::cout << std::boolalpha << t1.joinable() << '\n';
std::thread t2(work);
std::cout << t2.joinable() << '\n';
t2.join();
std::cout << t2.joinable() << '\n';
}
輸出:
false
true
Work Done!
false
注意:joinable() 並不是“執行緒現在是不是還在執行”。
即使執行緒函式已經執行完,只要還沒有對對應的 std::thread 呼叫 join() 或 detach(),它仍然可能:
thread.joinable() == true
比如:
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
void work()
{
std::cout << "Work Done!" << std::endl;
}
int main()
{
std::thread t1;
std::cout << std::boolalpha << t1.joinable() << '\n';
std::thread t2(work);
std::cout << t2.joinable() << '\n';
std::this_thread::sleep_for(100ms);
std::cout << t2.joinable() << '\n';
t2.join();
std::cout << t2.joinable() << '\n';
}
結果是
false
true
Work Done!
true
false
這裡明顯任務早就結束了,但延時100ms後去看joinable()發現還是true。
所以joinable()看的是他現在能不能被join()或者detach(),上面兩個例子中:
當這個執行緒物件使用預設構造,或者說沒有被給一個可呼叫物件的時候,這時候是false;
還有這個執行緒物件已經被join()或者detach()的時候,也會變成false。
2.建構函式:第一個引數是什麼?後面的引數是什麼?
最常見的構造形式可以概念化為:
std::thread t(callable, arg1, arg2, ...);
其中:
- 第一個引數:要線上程中執行的可呼叫物件;
- 後續引數:傳給這個可呼叫物件的實參。
可呼叫物件可以是:
- 普通函式;
- Lambda;
- 函式物件;
- 成員函式指標。
執行緒建立成功後,新執行緒會開始呼叫這個可呼叫物件。
2.1.普通函式
#include <iostream>
#include <thread>
void print_sum(int a, int b)
{
std::cout << a + b << '\n';
}
int main()
{
std::thread t(print_sum, 10, 20);
t.join();
}
這裡:
print_sum
是第一個引數,即執行緒函式;
10, 20
是傳給 print_sum 的引數。
2.2.Lambda
#include <iostream>
#include <thread>
int main()
{
std::thread t([](int value) {
std::cout << value << '\n';
}, 42);
t.join();
}
Lambda 本身就是第一個可呼叫物件引數,42 是它的實參。
2.3.函式物件
#include <iostream>
#include <thread>
struct Worker
{
void operator()(int value) const
{
std::cout << value << '\n';
}
};
int main()
{
std::thread t(Worker{}, 7);
t.join();
}
2.4.成員函式
呼叫成員函式時,除了成員函式指標,還必須告訴執行緒“在哪個物件上呼叫”。
#include <iostream>
#include <thread>
class Worker
{
public:
void run(int value)
{
std::cout << value << '\n';
}
};
int main()
{
Worker worker;
std::thread t(&Worker::run, &worker, 100);
t.join();
}
這裡:
&Worker::run
是成員函式指標;
&worker
表示在哪個物件上呼叫;
100
才是 run(int) 自己的引數。
3.執行緒引數預設會儲存副本
這是 std::thread 最容易讓初學者誤解的地方之一。
例如:
void foo(int value);
int x = 10;
std::thread t(foo, x);
執行緒並不是簡單地“記住表示式 x”。標準庫會把傳入引數經過衰減後儲存進執行緒內部,再在新執行緒中呼叫。
因此很多情況下,執行緒拿到的是引數副本。
3.1.按值傳遞
#include <iostream>
#include <thread>
void change(int value)
{
value = 100;
}
int main()
{
int x = 10;
std::thread t(change, x);
t.join();
std::cout << x << '\n';
}
輸出仍然是:
10
因為執行緒修改的是自己的引數副本。
4.std::ref():真正按引用傳遞
如果執行緒函式要求:
void change(int& value);
並希望修改原物件,應使用:
std::ref(x)
例如:
#include <functional>
#include <iostream>
#include <thread>
void change(int& value)
{
value = 100;
}
int main()
{
int x = 10;
std::thread t(change, std::ref(x));
t.join();
std::cout << x << '\n';
}
輸出:
100
std::ref(x) 返回的是一個:
std::reference_wrapper<T>
它本身可以被複制,但呼叫可呼叫物件時會還原為對原物件的引用。
4.1.std::cref()
如果需要按 const T& 傳遞,可以使用:
std::cref(x)
例如:
void print(const std::string& text);
std::string message = "hello";
std::thread t(print, std::cref(message));
5.std::ref(x) 和 &x 完全不是一回事
這兩個寫法經常被混淆。
std::ref(x)
表示“以引用語義傳遞原物件”。
而:
&x
是取地址,型別是指標。
例如:
void f1(int& value);
void f2(int* value);
int x = 10;
std::thread t1(f1, std::ref(x));
std::thread t2(f2, &x);
兩者對應的是不同的形參型別。
6.Lambda 捕獲也要注意值和引用
執行緒和 Lambda 經常配合使用。
按值捕獲:
int x = 10;
std::thread t([x] {
std::cout << x << '\n';
});
Lambda 內儲存自己的 x 副本。
按引用捕獲:
int x = 10;
std::thread t([&x] {
x = 20;
});
t.join();
執行緒直接訪問外部的 x。
按引用捕獲不會延長 x 的生命週期,因此必須確保執行緒使用 x 時,x 仍然存在。
7.join():等待執行緒結束
t.join();
表示:
當前執行緒在這裡阻塞,直到
t所關聯的執行緒執行結束。
例如:
#include <chrono>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
void work()
{
std::this_thread::sleep_for(1s);
std::cout << "worker done\n";
}
int main()
{
std::thread t(work);
std::cout << "before join\n";
t.join();
std::cout << "after join\n";
}
執行關係是:
创建 t
↓
work 与 main 并发执行
↓
main 到达 join
↓
等待 work 结束
↓
join 返回
↓
main 继续执行
呼叫成功後:
t.joinable() == false
7.1.join() 不會自動延長任意物件生命週期
join() 只是在它被呼叫的位置等待目標執行緒。
下面是安全的:
int x = 10;
std::thread t([&x] {
++x;
});
t.join();
// 到这里线程已经结束,而 x 还活着
但如果物件先離開作用域,再去等待執行緒,就已經太遲:
std::thread t;
{
int x = 10;
t = std::thread([&x] {
// 可能访问 x
});
} // x 在这里销毁
// 此时线程可能仍在访问已经销毁的 x
t.join();
8.detach():執行緒脫離管理
t.detach();
官方定義:呼叫 detach() 後,執行緒會繼續獨立執行,而當前 std::thread 物件不再與該執行緒關聯。
為了方便理解,也可以“民辦”地理解成:
detach() 一呼叫,當前執行緒物件就會立刻變成不可 joinable();
它有點像一個“不會阻塞的 join()”——區別在於,join() 會等待執行緒執行結束後再返回,而 detach() 不會等待,呼叫後立即返回,讓執行緒自己在後臺繼續執行。
因此:
t.joinable() == false
detach() 最大的問題是生命週期很難管理。
錯誤示例:
void start()
{
int value = 42;
std::thread t([&value] {
std::cout << value << '\n';
});
t.detach();
} // value 已经销毁,但脱离线程可能仍在使用它
這會產生懸空引用和未定義行為。
初學和普通工程程式碼中,優先考慮明確 join(),不要為了“讓執行緒後臺跑”就習慣性 detach()。
9.joinable():判斷能否 join
if (t.joinable())
{
t.join();
}
常見狀態變化:
| 操作 | joinable() |
|---|---|
預設構造 std::thread t; |
false |
std::thread t(f); |
true |
| 執行緒函式已經執行完,但尚未 join/detach | true |
呼叫 join() 後 |
false |
呼叫 detach() 後 |
false |
| 被 move-from 的 thread | false |
10.為什麼忘記 join/detach 會直接終止程式?
如果一個 std::thread 物件析構時仍滿足:
t.joinable() == true
程式會呼叫:
std::terminate()
這個std::terminate()會立刻終止整個程式。它不是“結束當前執行緒”,而是整個程序直接進入異常終止流程。
例如:
#include <thread>
void work()
{
}
int main()
{
std::thread t(work);
} // 错误:t 析构时仍然 joinable
可以把規則概念化成:
~thread()
{
if (joinable())
{
std::terminate();
}
}
這不是標準庫原始碼,只是幫助理解析構語義。
標準庫不會偷偷幫你自動 join(),因為自動等待可能讓程式在一個意料之外的位置長時間阻塞;也不會偷偷 detach(),因為那可能製造更嚴重的生命週期錯誤。
11.std::thread 不可複製,但可以移動
執行緒的管理權不能被兩個 std::thread 物件同時擁有,因此它不可複製:
std::thread t1(work);
// std::thread t2 = t1; // 编译错误
但可以轉移所有權:
std::thread t1(work);
std::thread t2 = std::move(t1);
移動後:
t1.joinable() == false
t2.joinable() == true
最後應該由 t2 負責 join() 或 detach()。
這也是為什麼 std::vector<std::thread> 可以工作:容器通過移動而不是複製管理執行緒物件。
12.std::this_thread
std::this_thread 提供“操作當前執行緒”的工具。但他不是隻能在std::thread的物件內使用。
12.1.獲取執行緒 ID
std::this_thread::get_id()
示例:
#include <iostream>
#include <thread>
int main()
{
std::cout << std::this_thread::get_id() << '\n';
}
12.2.休眠
std::this_thread::sleep_for(...)
std::this_thread::sleep_until(...)
| 函式 | 引數 | 含義 |
|---|---|---|
sleep_for(duration) |
時間長度 | 再睡多久 |
sleep_until(time_point) |
時間點 | 睡到什麼時候 |
例如:
//睡100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));
//到相对于now()之后的100ms
auto next = std::chrono::steady_clock::now();
next += 100ms;
std::this_thread::sleep_until(next);
12.3.主動讓出執行機會
std::this_thread::yield();
它只是向排程器提示“當前執行緒願意暫時讓出執行機會”,並不保證另一個指定執行緒一定馬上執行,也不保證下一個執行的執行緒不是自己。
比較形象一點的說法:當前執行緒主動告訴作業系統排程器:“我現在可以先讓一下 CPU,你可以考慮讓別的執行緒先執行。”
yield() 只是“主動讓出這次 CPU 排程機會”,但馬上又被排程回來也是完全可能的。
比如:
while (!ready)
{
std::this_thread::yield();
}
這種比while (!ready) {}這種空轉要好很多,因為空轉會一直霸佔 CPU,而 yield() 會給其他執行緒更多執行機會。
13.hardware_concurrency()
std::thread::hardware_concurrency()
一般用來查詢:當前系統大概支援多少個執行緒可以並行執行。一般是CPU 的邏輯核心數 / 硬體執行緒數。
#include <iostream>
#include <thread>
int main()
{
std::cout << "建议并发线程数:" << std::thread::hardware_concurrency() << '\n';
}
假設你的 CPU 是 8 核 16 线程,那麼 std::thread::hardware_concurrency() 通常會返回 16。
但這並不意味著程式最多隻能建立 16 個執行緒。它表示的是當前硬體大約能夠支援多少個執行緒真正並行執行,也就是硬體併發能力的參考值。 即使只有 16 個邏輯處理器,只要系統資源允許,依然可以建立幾十個甚至上百個執行緒。
當執行緒數量超過硬體能夠真正並行執行的數量時,作業系統會通過時間片輪轉、執行緒排程等機制,讓這些執行緒交替獲得 CPU 執行時間,從宏觀上看起來像是在“同時執行”。雖然這不是真正意義上的完全並行,但在很多場景下,從程式執行效果上看和真正併發並沒有特別明顯的區別。(類似STM32上使用的FreeRTOS,用1個CPU核心來模擬多執行緒)
另外,如果函式返回 0,表示當前實現無法提供硬體併發資訊,因此不能把這個返回值當作絕對可靠的固定值。
14.多執行緒輸出順序是不確定的
#include <iostream>
#include <thread>
void print(char c)
{
for (int i = 0; i < 5; ++i)
{
std::cout << c;
}
}
int main()
{
std::thread t1(print, 'A');
std::thread t2(print, 'B');
t1.join();
t2.join();
}
不能依賴輸出一定是:
AAAAA
BBBBB
完全有可能是
BBBBB
AAAAA
等很多可能性。

執行緒排程順序由作業系統和執行環境決定。如果執行順序本身是程式正確性的必要條件,就必須顯式同步。
15.常見錯誤
15.1.錯誤 1:忘記 join 或 detach
std::thread t(work);
// 离开作用域前没有处理 t
結果可能是 std::terminate()。
15.2.錯誤 2:把引用形參誤認為 thread 會自動按引用傳遞
void foo(int& x);
int value = 10;
// std::thread t(foo, value); // 不要把它理解成自动引用传递
需要原物件引用語義時寫:
std::thread t(foo, std::ref(value));
15.3.錯誤 3:detach 後引用區域性變數
區域性變數可能線上程使用前就銷燬。
15.4.錯誤 4:重複 join
t.join();
// t.join(); // 此时已经不再 joinable
15.5.錯誤 5:認為執行緒函式結束後 joinable 自動變 false
執行緒執行完和執行緒物件解除關聯是兩件不同的事情。
16.C++20:優先了解 std::jthread
std::thread 的一個明顯風險是:必須手動確保所有控制路徑最終都正確 join() 或 detach()。
C++20 提供了:
std::jthread
它在析構時會自動請求停止並等待執行緒結束,而且支援 std::stop_token 協作式取消。
基礎原理理解完 std::thread 後,建議繼續學習本章後面的 std::jthread 與 std::stop_token。
17.小結
std::thread t(f, args...)的第一個引數是可呼叫物件,後面的引數是傳給它的實參。- 執行緒引數預設會被儲存,引用語義通常要顯式使用
std::ref()/std::cref()。 join()等待執行緒結束,並讓執行緒物件變為 non-joinable。detach()讓執行緒脫離當前std::thread物件,生命週期風險較大。joinable()判斷物件是否仍關聯一個可 join 的執行緒,不等於執行緒此刻是否正在執行。- joinable 的
std::thread析構會呼叫std::terminate()。 std::thread不可複製,但可以移動。- 執行緒共享引用、指標或物件時,必須額外考慮物件生命週期和資料競爭。