跳到正文

Wiki

jthread 與 stop_token

約 10 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

C++20 提供了:

std::jthread

它可以理解成一個更適合現代 C++ 的線程類

std::thread 相比,它最重要的改進有兩個:

1. 生命周期更安全
   → 析构时自动处理线程,不容易因为漏写 join() 而出问题

2. 自带协作式停止机制
   → 可以通过 stop_token / request_stop() 请求线程安全退出

所以這一節最重要的是把兩個概念分開:

线程生命周期管理
→ 谁负责等待线程结束

线程停止协议
→ 谁发出停止请求,工作线程怎么响应

std::jthread 同時解決了這兩個問題,但它並不是“自動強殺線程”的工具。


1.為什麼需要 std::jthread

1.1.std::thread 的問題

使用普通 std::thread

std::thread thread(work);

在線程對象析構之前,必須保證它已經:

thread.join();

或者:

thread.detach();

否則,如果一個仍然 joinable()std::thread 被析構,程序會調用:

std::terminate();

例如:

#include <print>
#include <thread>

void work()
{
    std::println("working");
}

int main()
{
    std::thread thread(work);

    // 如果这里忘记 join() / detach()
}

程序離開 main() 時,thread 仍然是 joinable 的,於是會觸發 std::terminate()

真正麻煩的是,當程序中出現:

异常
提前 return
多个 if / else 分支
复杂资源管理

就更容易漏掉 join()

這和手動管理 new/delete 有點像:

代码简单时看起来没问题

控制路径一复杂
→ 很容易忘记收尾

而現代 C++ 更傾向於通過 RAII 自動管理資源。

線程也一樣。


1.2.std::jthread 會自動處理線程結束

最簡單的例子:

#include <print>
#include <thread>

void work()
{
    std::println("working");
}

int main()
{
    std::jthread thread(work);
}

運行結果:

working

這裏沒有寫:

thread.join();

但程序仍然可以正常結束。

因為當:

std::jthread thread;

離開作用域時,如果它仍然是 joinable 的,析構函數會負責收尾。

可以先粗略理解成:

std::thread
→ 需要自己保证 join() / detach()

std::jthread
→ 析构时自动处理

不過 jthread 的析構不只是簡單地 join()

更準確地説,如果它仍然 joinable,析構過程概念上會做:

request_stop()

join()

也就是:

先請求線程停止,然後等待線程真正結束。

這也是 jthread 和普通 thread 一個很重要的區別。


1.3.jthread 不會強制殺死線程

std::jthread 使用的是:

協作式停止(cooperative cancellation)

所謂“協作式”,意思是:

某个线程:
发出停止请求

工作线程:
看到停止请求

工作线程:
自己选择一个安全的位置退出

而不是:

request_stop()

操作系统立刻把工作线程强行杀掉

標準庫不會隨便把線程從某條機器指令中間直接掐死。

原因很簡單。

線程可能正在:

持有 mutex
修改共享数据
写文件
操作硬件
分配 / 释放资源
修改对象内部状态

如果在任意位置強制終止線程,很容易把程序狀態搞壞。

所以:

thread.request_stop();

更像是在告訴工作線程:

“你該停了,請在合適的位置自己退出。”

而不是:

“現在立刻把你殺掉。”


2.stop_token 停止協議

2.1.std::stop_token

2.1.1.stop_token 是什麼

std::stop_token 可以理解成:

停止請求的觀察端。

它本身不能發出停止請求,只能查看:

有没有人要求我停止?

例如:

stop_token.stop_requested();

返回:

true  → 已经有人请求停止
false → 目前还没有停止请求

jthread 內部有一個停止狀態,可以粗略理解成:

jthread / stop_source

   发出停止请求

   共享停止状态


   查看停止请求

    stop_token

也就是説:

request_stop()
→ 写入“停止请求”

stop_requested()
→ 查看“停止请求”

2.2.jthread 可以自動傳入 stop_token

來看一個最典型的例子:

#include <chrono>
#include <print>
#include <stop_token>
#include <thread>

using namespace std::chrono_literals;

void worker(std::stop_token token)
{
    while (!token.stop_requested())
    {
        std::println("working");
        std::this_thread::sleep_for(100ms);
    }

    std::println("stopping");
}

int main()
{
    std::jthread thread(worker);

    std::this_thread::sleep_for(350ms);

    thread.request_stop();
}

可能輸出:

working
working
working
working
stopping

這裏最特別的是:

void worker(std::stop_token token)

但創建線程時卻只寫了:

std::jthread thread(worker);

我們並沒有手動傳:

worker(token);

因為 std::jthread 發現這個函數可以接收 std::stop_token,於是會自動把自己的 stop token 傳進去。

可以近似理解成:

std::jthread thread(worker);

内部效果类似:

worker(jthread 自动提供的 stop_token);

這裏不可以顯式填參數,比如std::jthread thread(worker, thread.get_stop_token());, 這是非法的,原因非常直接:在執行構造函數參數時,thread 對象還沒有構造完成,所以你還不能調用thread.get_stop_token()

所以這裏直接記住他會自動把自己的stop token傳入進去就行了,不需要我們手動傳入。


2.3.request_stop()

調用:

thread.request_stop();

表示:

向這個 jthread 關聯的停止狀態發出停止請求。

它只負責“發請求”。

它不會:

强制终止线程
自动跳出 while
直接让 worker return
等待线程结束

真正決定什麼時候退出的是工作線程。

例如:

while (!token.stop_requested())
{
    do_work();
}

每輪循環檢查:

token.stop_requested()

一旦發現停止請求,就退出循環。

整個流程是:

main

│ 创建 jthread

worker 开始执行

│ while (!token.stop_requested())

│ working...

│ working...


main 调用 request_stop()


停止状态变成“已请求停止”


worker 下一次检查 stop_requested()


发现为 true


退出循环

所以最重要的一句話是:

request_stop() 只是請求停止,真正停止必須由工作線程自己配合。


2.4.request_stop() 的返回值

request_stop() 會返回一個 bool

bool result = thread.request_stop();

可以粗略理解成:

true
→ 这次调用真的把状态从
  “还没请求停止”
  改成了
  “已经请求停止”

false
→ 之前已经请求过停止
  或者当前停止状态无法产生停止请求

普通業務代碼經常不需要關心這個返回值。

直接寫:

thread.request_stop();

就可以。


2.5.stop_possible()

stop_token 還可以調用:

token.stop_possible();

它表示:

這個 token 是否關聯着一個“有可能收到停止請求”的停止狀態。

例如:

std::stop_token token;

這是一個默認構造的 token,它沒有關聯正常的停止狀態。

此時通常:

token.stop_possible()  == false
token.stop_requested() == false

std::jthread 自動提供的 token 通常是:

stop_possible() == true

對於普通使用來説,最常用的還是:

stop_requested()

stop_possible() 更多是在需要判斷“這個 token 到底支不支持停止協議”時使用。


2.6.jthread 析構時到底發生什麼

假設:

{
    std::jthread thread(worker);
}

執行到右花括號:

}

thread 開始析構。

如果它仍然是 joinable 的,那麼概念上會:

thread.request_stop();

thread.join();

所以:

{
    std::jthread thread(worker);
}

可以理解成退出作用域時自動做了:

thread.request_stop();
thread.join();

這也是為什麼這種代碼可以正常工作:

#include <print>
#include <stop_token>
#include <thread>

void worker(std::stop_token token)
{
    while (!token.stop_requested())
    {
        // work
    }

    std::println("stopped");
}

int main()
{
    std::jthread thread(worker);
}

main() 結束時:

jthread 析构

request_stop()

worker 看到停止请求

worker 退出

join() 等待结束

析构完成

2.7.但析構也不能“憑空讓線程退出”

下面這個線程完全不檢查 stop_token

void worker(std::stop_token)
{
    for (;;)   //死循环,效率比while(true)高
    {
    }
}

然後:

int main()
{
    std::jthread thread(worker);
}

離開 main() 時:

jthread 析构

request_stop()

停止请求确实发出了

但是 worker 根本不检查

for(;;) 继续运行

析构函数接着 join()

join() 一直等

程序就會一直卡在那裏。

因此:

jthread 提供的是停止協議,不是強制終止能力。

所以對於長期運行的 worker,一般應該在合適的位置檢查:

token.stop_requested()

例如:

while (!token.stop_requested())
{
    do_some_work();
}

而不是隻在線程開頭檢查一次:

if (token.stop_requested())
{
    return;
}

// 后面运行几个小时都不再检查

2.8.阻塞操作也會影響停止速度

即使代碼寫成:

while (!token.stop_requested())
{
    blocking_operation();
}

也不代表線程一定能馬上停止。

假設:

blocking_operation();

一次會阻塞 30 秒。

那麼即使外部已經執行:

thread.request_stop();

工作線程也必須先從:

blocking_operation();

返回,才能進行下一輪:

token.stop_requested()

所以停止響應速度還取決於工作線程內部在幹什麼。

對於需要快速停止的線程,要考慮:

阻塞函数有没有超时

阻塞操作能不能被唤醒

能不能把长任务拆成多个小步骤

能不能在多个安全点检查 stop_requested()

例如:

while (!token.stop_requested())
{
    do_small_step();
}

通常比:

while (!token.stop_requested())
{
    do_one_huge_blocking_job();
}

更容易快速響應停止請求。


2.9.std::stop_source

前面説過:

stop_token
→ 观察停止请求

那麼誰負責發出停止請求?

除了 jthread.request_stop(),標準庫還提供:

std::stop_source

它可以理解成:

停止請求的控制端。

例如:

#include <print>
#include <stop_token>

int main()
{
    std::stop_source source;

    std::stop_token token = source.get_token();

    std::println("{}", token.stop_requested());

    source.request_stop();

    std::println("{}", token.stop_requested());
}

運行結果:

false
true

整個關係可以理解成:

stop_source

    │ request_stop()

共享停止状态

    │ stop_requested()
stop_token

所以:

std::stop_source source;

負責:

发请求

而:

std::stop_token token = source.get_token();

負責:

看请求

2.10.多個 stop_token 可以觀察同一個停止狀態

例如:

std::stop_source source;

std::stop_token token1 = source.get_token();
std::stop_token token2 = source.get_token();
std::stop_token token3 = source.get_token();

這三個 token 都關聯到同一個停止狀態。

當:

source.request_stop();

之後:

token1.stop_requested()
token2.stop_requested()
token3.stop_requested()

都會變成:

true

所以 stop_source + stop_token 很適合表達:

一个管理者

统一发出停止请求

多个任务 / 对象

一起观察同一个停止状态

2.11.std::stop_callback

有時候我們不想一直手動檢查:

token.stop_requested()

而是希望:

一旦停止請求出現,就自動執行某個操作。

可以使用:

std::stop_callback

例如:

#include <print>
#include <stop_token>

int main()
{
    std::stop_source source;
    std::stop_token token = source.get_token();

    std::stop_callback callback(token, [] {
        std::println("stop requested");
    });

    source.request_stop();
}

運行結果:

stop requested

也就是説:

request_stop()

停止状态改变

已经注册的 stop_callback 被调用

他如果是jthread調用的話,那麼順序是:

request_stop()

同步执行 stop_callback

request_stop() 返回

join()

也就是説,callback 先於 join() 完成。std::jthread 的析構本身就是先調用 request_stop(),再調用 join()。

它很適合幹一些很輕量的事情,例如:

设置状态

唤醒某个等待对象

通知某个组件开始收尾

但不建議在 callback 裏面做:

复杂计算
长时间 IO
长时间阻塞
大量锁操作

因為停止路徑本身應該儘量簡單、可控。

還有一個細節:

如果註冊 callback 時,停止請求已經發生,那麼 callback 可能會在註冊過程中直接執行。

所以不要假設:

callback 一定会在未来某个时间
由另一个线程执行

3.jthread 的線程函數和生命週期操作

3.1.jthread 不一定非要使用 stop_token

std::jthread 也可以像普通 std::thread 一樣運行普通函數。

例如:

#include <print>
#include <thread>

void work(int value)
{
    std::println("{}", value);
}

int main()
{
    std::jthread thread(work, 42);
}

運行結果:

42

這裏:

void work(int value)

根本沒有 stop_token

沒有關係。

jthread 會判斷這個函數能不能接收自動提供的 stop token。

可以簡單理解成:

如果函数可以这样调用:

worker(stop_token, args...)

→ 自动传入 stop_token

否则如果函数只能这样调用:

worker(args...)

→ 就像普通 thread 一样调用

所以兩種寫法都可以:

void worker(std::stop_token token);

以及:

void worker();

3.2.帶額外參數的 stop_token 線程函數

例如:

#include <print>
#include <stop_token>
#include <thread>

void worker(std::stop_token token, int id)
{
    std::println("worker {}", id);

    while (!token.stop_requested())
    {
        // work
    }
}

int main()
{
    std::jthread thread(worker, 7);

    thread.request_stop();
}

這裏:

std::jthread thread(worker, 7);

可以理解成最終調用:

worker(自动提供的 stop_token, 7);

所以如果函數需要 stop token,參數順序通常是:

void worker(std::stop_token token, 参数1, 参数2, ...)

例如:

void worker(
    std::stop_token token,
    int id,
    std::string name);

創建:

std::jthread thread(worker, 7, "camera");

相當於:

worker(
    自动提供的 stop_token,
    7,
    "camera"
)

3.3.jthread 仍然可以手動 join()

雖然 jthread 會自動 join,但你仍然可以自己寫:

std::jthread thread(work);

thread.join();

執行完:

thread.join();

以後:

thread.joinable()

會變成:

false

因為這個 jthread 已經不再關聯一個可 join 的執行線程。

所以:

自动 join

並不代表:

不能手动 join

它只是意味着:

如果你沒手動處理,析構函數會幫你安全收尾。


3.4.jthread 也可以 detach(),但通常不推薦

jthread 仍然提供:

thread.detach();

但這樣做以後:

jthread 不再拥有这个线程

析构时也没办法再 join 它

於是 jthread 最重要的 RAII 生命週期優勢基本就沒有了。

所以如果你發現自己準備寫:

thread.detach();

最好先問幾個問題:

这个线程以后由谁负责?

程序退出时它怎么结束?

它访问的对象会不会已经析构?

它访问的资源能活多久?

除非你非常明確地知道線程生命週期怎麼管理,否則一般不建議隨便 detach。


3.5.condition_variable_anystop_token

前面提到一個問題:

如果線程正在:

condition_variable.wait(...)

裏面睡眠,那麼只檢查:

token.stop_requested()

是不夠的。

因為線程可能根本沒機會執行到下一次檢查。

C++20 的:

std::condition_variable_any

提供了可以直接配合 stop_token 的等待方式。

例如:

#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)
    {
        // 没等到 ready == true,
        // 而是因为停止请求结束等待
        return;
    }

    // ready == true
}

可以把這次等待理解成:

等待两个条件中的任意一个:

1. ready == true

或者

2. 收到停止请求

如果:

condition_met == true

表示謂詞:

ready == true

成立。

如果:

condition_met == false

説明這次等待並不是因為 ready 成立而結束,通常就是因為收到了停止請求。

這樣工作線程即使正在條件變量裏等待,也能夠響應 jthread 的停止協議。


4.選擇與常見錯誤

4.1.std::threadstd::jthread 對比

特性 std::thread std::jthread
引入版本 C++11 C++20
創建線程
手動 join()
手動 detach()
析構時自動收尾
joinable 狀態析構會 terminate()
自帶停止請求機制
自動提供 stop_token
適合長期可停止 worker 一般

如果項目可以使用 C++20,對於這種線程:

启动

持续运行

程序退出时请求停止

安全结束

通常優先考慮:

std::jthread

4.2.atomic<bool> runningstop_token 怎麼選

傳統代碼經常這樣寫:

std::atomic<bool> running{true};

void worker()
{
    while (running.load())
    {
        // work
    }
}

停止時:

running.store(false);

這種寫法沒有問題。

對於非常簡單的線程,它甚至很直觀。

例如:

running == true
→ 继续运行

running == false
→ 停止

但是:

std::stop_token

提供的是一套標準化的停止協議。

它可以和:

std::jthread

std::stop_source

std::stop_callback

std::condition_variable_any

直接配合。

可以理解成:

atomic<bool>

→ 只是一个线程安全的 bool
→ “true / false 到底是什么意思”由你自己规定

而:

stop_token

→ 专门表达“停止请求”
→ 标准库里的多个工具都认识这种协议

因此:

非常简单的开关
→ atomic<bool> 完全可以

明确的线程停止生命周期
→ jthread + stop_token 更合适

4.3.常見錯誤

4.3.1.錯誤:以為 request_stop() 會強制殺死線程

錯誤理解:

request_stop()
→ 线程立刻消失

正確理解:

request_stop()
→ 发出停止请求
→ 工作线程自己检查并退出

4.3.2.錯誤:worker 從來不檢查 stop_requested()

例如:

void worker(std::stop_token)
{
    while (true)
    {
        do_work();
    }
}

即使 jthread 析構時自動:

request_stop();

這個線程也不會退出。

最終析構中的:

join();

會一直等。


4.3.3.錯誤:檢查了停止請求,但中間阻塞太久

例如:

while (!token.stop_requested())
{
    blocking_operation(); // 一卡几十秒
}

停止請求雖然已經發出,但線程要等到阻塞操作返回後才能再次檢查。


4.3.4.錯誤:用了 jthread 又隨手 detach()

這樣會失去:

自动 join
自动停止请求
RAII 生命周期管理

這些主要優勢。


4.3.5.錯誤:在 stop_callback 裏做大量複雜工作

callback 更適合:

轻量通知
设置状态
唤醒等待者

而不是承擔複雜業務邏輯。


4.3.6.錯誤:以為“收到停止請求”就會立刻終止線程(重要)

request_stop() 只是發出停止請求,並不會強制中斷工作線程。

工作線程通常會在某些安全檢查點主動檢查:

token.stop_requested()

例如:

while (!token.stop_requested())
{
    finish_one_small_task();
}

如果停止請求恰好在 finish_one_small_task() 執行過程中到來,這個函數不會被強行執行到一半就終止。

線程通常會先完成當前這一小塊工作,然後回到循環條件:

token.stop_requested()

發現停止請求後,再退出循環並結束線程。

可以理解成:

收到停止请求

完成当前不能被随意打断的小任务

到达安全检查点

发现 stop_requested() == true

退出循环

完成必要的收尾

线程结束

join() 的作用只是:

等待這個線程真正結束。

所以典型關係是:

request_stop()
→ 请求线程停止

worker 在安全位置响应请求并退出

join()
→ 等待 worker 退出完成

所謂“合適、安全的位置”,就是線程狀態已經完整、資源處於一致狀態,此時退出不會留下半完成操作的位置。

安全點 = 可以開始退出的位置;函數 return = 線程真正退出。

所以可以這麼理解例子中的worker函數 返回 ≈ 這個線程執行結束。


5.小結

std::jthread 可以理解成:

帶 RAII 生命週期管理和標準停止協議的 std::thread

最重要的幾個點:

std::thread
→ 必须自己保证 join() / detach()

std::jthread
→ 析构时自动 request_stop() + join()

停止機制:

request_stop()
→ 发出停止请求

stop_token.stop_requested()
→ 查看是否收到停止请求

它是:

协作式停止

而不是:

强制杀死线程

另外:

stop_source
→ 发出停止请求

stop_token
→ 观察停止请求

stop_callback
→ 停止请求出现时执行回调

對於 C++20 中“啓動後持續運行、退出時需要安全停止”的後台線程,一般可以優先考慮:

std::jthread + std::stop_token

而不是:

std::thread + 手写生命周期管理 + 自定义 atomic<bool> 停止协议