jthread 與 stop_token
C++20 提供了:
std::jthread
它可以理解成“更安全的 std::thread”:
- 析構時自動處理線程,不要求每條控制路徑都手寫
join(); - 內置協作式停止機制;
- 可以與
std::stop_token、std::stop_source、std::stop_callback配合。
如果項目使用 C++20,並且線程是“啓動後持續運行,退出時需要安全停止”的類型,std::jthread 通常比裸 std::thread 更合適。
1. std::thread 的生命週期風險
使用 std::thread 時:
std::thread t(work);
必須保證對象析構前已經:
t.join();
或者:
t.detach();
否則 joinable 的 std::thread 析構會調用:
std::terminate()
這意味着異常、提前 return、複雜分支都可能讓“是否已經 join”變成維護負擔。
2. std::jthread 會自動 join
最簡單的寫法:
#include <iostream>
#include <thread>
void work()
{
std::cout << "working\n";
}
int main()
{
std::jthread t(work);
}
離開作用域時,不需要手動寫:
t.join();
jthread 的析構函數會負責等待線程結束。
這讓線程管理更符合 RAII 思想。
3. jthread 不等於“強制殺死線程”
std::jthread 的停止機制是:
協作式停止(cooperative cancellation)
也就是說:
某个线程发出停止请求
↓
工作线程主动检查这个请求
↓
工作线程自己决定在安全位置退出
標準庫不會粗暴地強制終止線程。
這是非常重要的設計,因爲強制殺線程可能發生在:
- 正持有 mutex;
- 正修改對象狀態;
- 正寫文件;
- 正執行資源管理代碼;
如果在任意位置突然中斷,很容易破壞程序狀態。
4. std::stop_token
std::stop_token 表示:
我可以觀察某個停止請求是否已經發出。
jthread 可以自動把 stop token 作爲線程函數的第一個參數傳入。
例如:
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
using namespace std::chrono_literals;
void worker(std::stop_token stop_token)
{
while (!stop_token.stop_requested())
{
std::cout << "working\n";
std::this_thread::sleep_for(100ms);
}
std::cout << "stopping\n";
}
int main()
{
std::jthread thread(worker);
std::this_thread::sleep_for(350ms);
thread.request_stop();
}
這裏線程函數的第一個參數:
std::stop_token
由 jthread 自動提供,不需要調用者手動傳。
5. request_stop()
調用:
thread.request_stop();
只是“發出停止請求”。
它不會等待線程結束,也不會強制終止線程。
工作線程需要主動檢查:
stop_token.stop_requested()
並自行退出。
6. stop_possible()
stop_token 還可以查詢:
stop_token.stop_possible()
表示這個 token 是否關聯着一個能夠產生停止請求的停止狀態。
普通 jthread 自動提供的 token 通常是可停止的。
7. jthread 析構時會發生什麼
如果一個 jthread 在析構時仍然 joinable,析構過程概念上會:
request_stop()
↓
join()
也就是說,它不只是自動等待,還會先請求線程停止。
因此:
{
std::jthread thread(worker);
} // 离开作用域时自动请求停止并等待结束
如果 worker 正確響應 stop token,就能自然退出。
8. 析構請求停止不等於一定能立刻退出
如果工作線程完全不檢查 stop token:
void worker(std::stop_token)
{
while (true)
{
}
}
那麼 jthread 析構時雖然調用了 request_stop(),線程依然不會退出。
隨後析構中的 join() 會一直等待。
所以:
jthread提供停止協議,但線程函數必須主動合作。
9. 阻塞操作也要考慮停止
下面雖然檢查了停止請求:
while (!stop_token.stop_requested())
{
blocking_operation();
}
但如果:
blocking_operation()
一次就能阻塞幾十秒,那麼停止響應仍然會非常慢。
線程設計不僅要考慮“有沒有 stop token”,還要考慮:
- 阻塞調用是否支持超時;
- 是否能被喚醒;
- 是否可以分階段檢查停止狀態。
10. std::stop_source
std::stop_source 是停止請求的“控制端”。
可以:
std::stop_source source;
std::stop_token token = source.get_token();
然後:
source.request_stop();
所有關聯到這個停止狀態的 token 都能觀察到請求。
示例:
#include <iostream>
#include <stop_token>
int main()
{
std::stop_source source;
std::stop_token token = source.get_token();
std::cout << std::boolalpha
<< token.stop_requested()
<< '\n';
source.request_stop();
std::cout << token.stop_requested() << '\n';
}
輸出:
false
true
11. std::stop_callback
有時不希望某個線程一直輪詢:
stop_requested()
而是希望停止請求到來時自動執行一段回調。
可以使用:
std::stop_callback
例如:
#include <iostream>
#include <stop_token>
int main()
{
std::stop_source source;
std::stop_token token = source.get_token();
std::stop_callback callback(token, [] {
std::cout << "stop requested\n";
});
source.request_stop();
}
request_stop() 發出停止請求時,已註冊的 callback 會執行。
注意 callback 的執行上下文與停止請求有關,因此回調本身也應該保持簡單,並避免製造新的鎖順序問題。
12. jthread 也支持普通線程函數
並不是所有 jthread 函數都必須接收 stop_token。
例如:
#include <iostream>
#include <thread>
void work(int value)
{
std::cout << value << '\n';
}
int main()
{
std::jthread thread(work, 42);
}
如果可調用對象能夠以:
(stop_token, args...)
形式調用,jthread 會優先傳入 stop token。
否則就像普通 thread 一樣使用:
(args...)
形式調用。
13. 帶參數的 stop token 線程函數
#include <iostream>
#include <stop_token>
#include <thread>
void worker(std::stop_token token, int id)
{
std::cout << "worker " << id << '\n';
while (!token.stop_requested())
{
// work
}
}
int main()
{
std::jthread thread(worker, 7);
thread.request_stop();
}
這裏:
std::jthread thread(worker, 7);
調用效果相當於讓線程執行:
worker(自动提供的 stop_token, 7)
14. jthread 同樣可以 join
自動 join 不代表不能手動 join。
std::jthread thread(work);
thread.join();
調用後:
thread.joinable() == false
之後析構時就無需再次等待。
同樣可以查詢:
thread.joinable()
thread.get_id()
15. jthread 也可以 detach,但通常不推薦
jthread 仍然提供:
thread.detach();
但一旦 detach,RAII 自動等待和結構化停止的優勢就基本失去了。
除非確實明確理解生命週期,否則不要爲了“後臺運行”輕易 detach。
16. condition_variable_any 與 stop token
普通 std::condition_variable 沒有直接接收 stop_token 的等待重載。
C++20 的:
std::condition_variable_any
則可以把停止請求整合進等待條件。
例如:
#include <condition_variable>
#include <mutex>
#include <stop_token>
#include <thread>
std::mutex mutex;
std::condition_variable_any cv;
bool ready = false;
void worker(std::stop_token token)
{
std::unique_lock lock(mutex);
bool condition_met = cv.wait(
lock,
token,
[] {
return ready;
});
if (!condition_met)
{
// 因停止请求结束等待
return;
}
// ready == true
}
這樣線程在等待條件時也能響應停止請求,不必額外寫一個輪詢循環。
17. thread 和 jthread 對比
| 特性 | std::thread | std::jthread |
|---|---|---|
| 標準版本 | C++11 | C++20 |
| 創建線程 | ✅ | ✅ |
手動 join() | ✅ | ✅ |
| 析構自動等待 | ❌ | ✅ |
| 析構時 joinable 會 terminate | ✅ | ❌ |
| 內置停止請求 | ❌ | ✅ |
自動傳 stop_token | ❌ | ✅ |
| 可 detach | ✅ | ✅ |
如果環境允許使用 C++20,新寫的“可停止後臺線程”通常優先考慮 jthread。
18. atomic<bool> running 和 stop token 怎麼選
傳統寫法:
std::atomic<bool> running{true};
while (running.load())
{
...
}
仍然完全合法,而且在簡單場景下很直觀。
stop_token 的優勢是把:
停止请求
停止观察
停止回调
jthread 生命周期
組織成標準協議。
因此:
- 只需要一個非常簡單的原子標誌:
atomic<bool>仍然很好用; - 線程本身有明確的“請求停止”生命週期:優先考慮
jthread + stop_token。
19. 常見錯誤
- 以爲
request_stop()會強制殺死線程。 - 工作函數從不檢查
stop_requested(),導致析構仍然一直等待。 - 在線程內部執行長時間不可中斷阻塞操作,導致停止響應很慢。
- 使用了
jthread卻仍然隨手detach(),失去 RAII 生命週期管理優勢。 - stop callback 內執行復雜阻塞邏輯或獲取大量鎖,導致停止路徑本身難以控制。
- 把“收到停止請求”理解成“必須立即在任意位置退出”,而不是在安全點協作退出。
小結
std::jthread是 C++20 更現代的線程管理類。jthread析構時會對 joinable 線程請求停止並等待結束。std::stop_token用來觀察停止請求,request_stop()只是請求,不是強制終止。- 停止必須由工作線程協作響應。
std::stop_source是請求端,std::stop_callback可以在請求發生時執行回調。- 對需要明確停止協議的後臺線程,
jthread + stop_token通常比thread + atomic<bool>更結構化。