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不可複製,但可以移動。- 線程共享引用、指針或對象時,必須額外考慮對象生命週期和數據競爭。