文章详情 学习记录

C++学习(十六)

发布时间:2026-09-21 12:17 浏览次数:68 最后更新:2026-09-21 12:17

c++20:std::stop token 与 std::jthread

正文内容

std::stop_token 解析

std::stop_token 是 C++20 的协作式取消机制。它不强制终止线程,而是由请求方发出停止请求,执行方在合适的位置自行检查并安全退出。

核心组件:

  • std::stop_source:持有“请求停止”的能力。
  • std::stop_token:可复制的只读观察句柄,交给任务或线程。
  • std::stop_callback:停止请求发出时同步执行的回调。
  • std::jthread:自动传入 stop_token,析构时会先请求停止、再 join。
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        // 做一小段可中断工作
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::cout << "Worker stopped safely.\n";
}

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

    std::this_thread::sleep_for(std::chrono::seconds(1));
    thread.request_stop();
} // jthread 析构:request_stop() + join()

request_stop() 的特点:

  • 是幂等操作:第一次通常返回 true,后续调用返回 false。
  • 请求一旦发出不可撤销。
  • 多个 stop_token 可以共享同一个停止状态。
  • 停止状态由最后一个关联的 stop_source、stop_token、stop_callback 释放后销毁。
  • 默认构造的 std::stop_token 没有关联停止状态,stop_possible() 为 false。
std::stop_source source;
std::stop_token token = source.get_token();

bool firstRequest = source.request_stop(); // true
bool alreadyRequested = source.request_stop(); // false

if (token.stop_requested()) {
    // 执行清理并返回
}

与 std::jthread 配合

当线程函数首个参数能接受 std::stop_token 时,jthread 会自动注入它:

std::jthread thread([](std::stop_token stopToken, int intervalMs) {
    while (!stopToken.stop_requested()) {
        // ...
    }
}, 50);

若线程函数不接受 token,jthread 仍可使用,只是任务无法感知停止请求。

相比 std::thread,std::jthread 通常更适合管理后台工作线程,因为它避免了析构时遗漏 join() 导致 std::terminate() 的问题。不过要注意:若任务永远不检查 token,jthread 析构仍会一直阻塞在 join()。

阻塞等待的正确写法

只在循环中轮询 stop_requested() 不够理想,因为任务可能卡在条件变量、队列或 I/O 等待中。C++20 的 condition_variable_any 支持直接传入 token:

#include <condition_variable>
#include <mutex>
#include <stop_token>

std::mutex mutex;
std::condition_variable_any condition;
bool hasWork = false;

void worker(std::stop_token stopToken) {
    std::unique_lock lock(mutex);

    condition.wait(lock, stopToken, [] {
        return hasWork;
    });

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

    // 处理工作
}

这会在停止请求到来时唤醒等待者。注意,这个带 token 的重载是 std::condition_variable_any 提供的,不是 std::condition_variable。

对于不支持 token 的阻塞 API,可注册回调来唤醒等待:

std::stop_callback callback(stopToken, [&] {
    condition.notify_all();
});

然后仍需在被唤醒后检查停止状态并退出。回调本身不应执行重活、阻塞操作或尝试获取可能被当前线程持有的锁。

std::stop_callback 的并发语义

这是最容易踩坑的部分:

std::stop_callback onStop(token, [] {
    // 收到停止请求时执行
});
  • 若停止尚未请求,回调会在另一个线程调用 request_stop() 时同步运行。
  • 若构造 stop_callback 时停止已经被请求,回调会在构造函数中立即、同步执行。
  • request_stop() 会等待正在执行的相关回调完成后才返回。
  • 因此回调不可抛异常、不可长期阻塞,也要警惕锁顺序导致死锁。

推荐将回调限制为“解除阻塞”:

std::stop_callback onStop(stopToken, [&queue] {
    queue.notify_waiters();
});

不推荐在其中直接关闭复杂资源、执行大规模清理或调用未知的用户代码。

推荐的任务函数结构

void processRequests(std::stop_token stopToken) {
    try {
        while (!stopToken.stop_requested()) {
            auto request = waitForRequest(stopToken);

            if (!request || stopToken.stop_requested()) {
                break;
            }

            process(*request);
        }
    } catch (...) {
        // 记录错误;必要时通知任务协调器
    }

    // 在这里执行本线程拥有资源的收尾工作
}

实践原则:

  • 在工作单元之间检查 stop_requested(),不要只在最外层检查一次。
  • 让每个工作单元足够短,或让内部算法也能接收 token。
  • 把 token 作为任务函数的第一个参数,保持与 std::jthread 的约定一致。
  • 资源清理放在任务正常退出路径中,而不是依赖回调完成。
  • 对长时间阻塞的操作,优先选择原生支持取消或超时的 API。
  • 取消是“请求”,不是“保证立刻停止”;API 文档应明确最大退出延迟。

常见误区

// 错误:这不是强制中断
source.request_stop();

调用后,线程不会自动消失;任务必须主动响应。

// 风险:任务没有读取 token,jthread 析构可能永远等待
std::jthread thread([] {
    for (;;) {
        doWork();
    }
});
// 风险:回调可能在构造期间同步执行
std::stop_callback callback(token, [&] {
    useObjectNotFullyInitialized();
});

应先完成对象初始化,再注册回调,或确保回调可安全处理该时机。

何时使用

适合:

  • 后台工作线程、轮询任务、任务队列消费者。
  • 可分段计算、批量处理、可取消算法。
  • 需要统一关闭协议的服务组件。

不适合直接解决:

  • 无法中断的第三方阻塞调用。
  • 需要强制终止失控线程的场景。
  • 跨进程或跨网络取消协议;这些仍需应用层取消消息或连接关闭策略。

一句话总结:std::stop_token 将线程取消从“共享原子标志的临时约定”升级为标准化、可组合、可唤醒阻塞等待的协作式取消协议;std::jthread 则是其最自然的线程生命周期载体。



std::jthread 讲解

std::jthread 是 C++20 引入的“自动管理生命周期的线程”。它和 std::thread 的主要差别是:

  1. 析构时自动 join(),不会因忘记 join() / detach() 而触发 std::terminate()。
  2. 支持协作式停止,内置 std::stop_source 与 std::stop_token。
  3. 当线程函数的第一个参数是 std::stop_token 时,会自动传入取消令牌。
#include <chrono>
#include <iostream>
#include <thread>

int main() {
    std::jthread worker([] {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "任务结束\n";
    });

} // 自动 join,确保线程结束后才离开作用域

等价于大致如下的 std::thread 写法:

std::thread worker([] {
    // 任务
});

if (worker.joinable()) {
    worker.join();
}

但 jthread 用 RAII 自动完成收尾,更不容易遗漏。

配合停止请求

#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>

void run(std::stop_token token) {
    while (!token.stop_requested()) {
        std::cout << "working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    std::cout << "收到停止请求,安全退出\n";
}

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

    std::this_thread::sleep_for(std::chrono::seconds(2));
    worker.request_stop();
}

调用关系:

jthread.request_stop()
        |
        v
内部 stop_source.request_stop()
        |
        v
线程函数中的 stop_token.stop_requested() == true
        |
        v
任务自行清理并退出

request_stop() 只是“请求”停止,不会强制杀死线程。线程函数必须定期检测:

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

否则 jthread 析构时仍会等待线程完成:

std::jthread worker([] {
    while (true) {
        doWork(); // 不检查 token,析构可能永久阻塞
    }
});

参数传递规则

当首个参数是 std::stop_token 时,jthread 自动注入 token:

void download(std::stop_token token, std::string url);

std::jthread worker(download, "https://example.com/data");

实际调用近似为:

download(worker.get_stop_token(), "https://example.com/data");

如果函数不接受 std::stop_token,也可以正常使用:

std::jthread worker([](int count) {
    for (int index = 0; index < count; ++index) {
        // ...
    }
}, 10);

只是 request_stop() 不会让该线程函数产生实际效果。

适合的场景

  • 后台任务、定时轮询、日志写入线程。
  • 生产者/消费者任务。
  • 服务关闭时需要优雅退出的工作线程。
  • 临时并发任务,作用域结束时必须等待完成。

使用建议

  • 新代码通常优先选 std::jthread,除非确实需要 detach()。
  • 将 std::stop_token 放在线程函数的第一个参数。
  • 将长任务拆成可中断的小步骤,周期性检查 stop_requested()。
  • 对条件变量等待,使用 std::condition_variable_any::wait(lock, token, predicate),这样停止请求可唤醒等待。
  • 不要在持有关键锁时依赖线程析构,因为析构中的 join() 可能等待一个同样需要该锁的线程,造成死锁。
std::mutex mutex;

void worker(std::stop_token token) {
    std::unique_lock lock(mutex);
    while (!token.stop_requested()) {
        // ...
    }
}

// 避免:持锁销毁 jthread,worker 可能正等待 mutex

一句话:std::jthread 用 RAII 保证线程会被等待结束,并通过 stop_token 提供标准的优雅取消通道。

正文内容已启用复制保护,代码块与行内代码仍支持复制。

评论区

欢迎参与讨论,评论提交与展示状态会在下方同步更新。

当前站点暂未开放评论提交功能。

读者留言

还没有评论,来发表第一条评论吧。

共 0 条评论 已展示 0 / 0 条评论
当前还没有评论,来发表第一条评论吧。若评论需要审核,通过后会展示在这里。