C++20:std::stop_source让线程说停就听

std::stop_source 是 C++20 引入的标准库组件(位于 <stop_token> 头文件中),用于在多线程编程中实现协同式线程取消(Cooperative Thread Cancellation)

它提供了一种线程安全、优雅且通用的机制,用来发起"停止请求",使运行中的异步任务或后台线程能够主动检查并安全退出。

背景对比 :在 C++20 之前,终止线程通常依赖手动维护 std::atomic<bool> 标志位,或者使用平台特定的 API(如 POSIX pthread_cancel)。然而平台强制终止极易导致 RAII 析构函数未运行、互斥锁死锁以及资源泄漏等未定义行为(UB)。


核心协作三元组

C++20 的协同取消机制由三个相互协作的组件构成:

  1. std::stop_source(信号控制端) :负责发起停止信号(调用 .request_stop())。
  2. std::stop_token(信号观察端) :传递给工作线程,用于轮询或检查是否收到了停止信号(调用 .stop_requested())。
  3. std::stop_callback(回调绑定机制) :允许注册回调函数,当停止请求被发起时同步触发,用于打断处于阻塞状态的 I/O 或条件变量。

一、 API 详解

1. std::stop_source 核心接口

核心控制与 Token 获取
  • **request_stop() -> bool**

  • 作用:发起停止请求。

  • 返回值 :首次成功发起停止请求返回 true;若此前已发起过停止请求,或当前对象未关联有效的底层状态,返回 false

  • 副作用 :使所有关联的 std::stop_token::stop_requested() 返回 true,并同步触发 所有已注册的 std::stop_callback

  • **get_token() -> std::stop_token**

  • 作用 :获取与当前 stop_source 绑定共享状态的 std::stop_token 对象,用于下发给工作线程。

状态查询
  • **stop_requested() const -> bool**

  • 作用:检查关联的底层状态是否已收到停止请求。

  • **stop_possible() const -> bool**

  • 作用 :检查当前对象是否仍有可能 发起停止请求。若关联了有效状态且未被全部释放,返回 true;若为默认构造的空对象,或所有关联的 stop_source 均已销毁,返回 false

生命周期与比较
  • 构造函数

  • stop_source():默认构造,创建一个新的可触发停止请求的底层共享状态。

  • stop_source(std::nostopstate_t):构造一个不带底层状态的空对象(stop_possible()false)。

  • 拷贝 / 移动构造 :支持拷贝与移动。由于底层使用引用计数管理状态,拷贝出的多个 stop_source 共享同一个底层状态。

  • **swap() / std::swap**:交换两个 stop_source 对象的底层状态。

  • 比较运算符 (== / !=) :判断两个 stop_source 是否共享同一个底层停止状态(或均为空)。


2. std::stop_token 核心接口

状态查询
  • **stop_requested() const -> bool**

  • 作用 :检查关联的 std::stop_source 是否已发起停止请求。

  • 典型用途 :作为工作线程主循环的退出条件(如 while (!stoken.stop_requested()))。

  • **stop_possible() const -> bool**

  • 作用 :检查当前 token 是否仍有可能 收到停止请求。若关联了有效状态且至少存在一个存活的 stop_source,返回 true;若所有 stop_source 均已析构,或 token 本身为无状态对象(nostopstate),返回 false

  • 优化场景 :在长循环中,若 stop_possible()false,可提前终止对 stop_requested() 的无效轮询。

生命周期与比较
  • 构造函数stop_token() 创建不包含底层状态的空 token(stop_possible()stop_requested() 均返回 false)。
  • 拷贝与移动 :支持轻量级拷贝与移动,多个 stop_token 可分发至多个并发线程,共享同一个状态观察点。
  • swap() 与比较运算符 (== / !=) :与 stop_source 行为一致,比较或交换底层共享状态。

3. std::stop_sourcestd::stop_token 对比

维度 std::stop_source std::stop_token
角色定义 信号控制者(控制端) 信号观察者(只读端)
核心职责 发起停止请求(request_stop() 检查停止状态(stop_requested()
线程归属 通常由主控线程 / 管理线程持有 传递给一个或多个后台工作线程
接口安全性 拥有修改权,可改变底层共享状态 完全只读,无法发起取消,天然编译期接口安全
生命周期依赖 只要存在一个存活的 stop_source,即可触发取消 即使所有 stop_source 均已析构,stop_token 仍可安全查询(但 stop_possible() 会变为 false

二、 代码示例

示例 1:配合 std::thread 或异步任务手动控制

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>

void worker(std::stop_token stoken) {
    while (!stoken.stop_requested()) {
        std::cout << "Worker running...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "Worker received stop request, cleaning up and exiting.\n";
}

int main() {
    std::stop_source source;

    // 将与其绑定的 stop_token 传给工作线程
    std::thread t(worker, source.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 主线程发起停止请求
    std::cout << "Main thread requesting stop...\n";
    source.request_stop();

    t.join();
    return 0;
}

示例 2:与 std::jthread 深度集成(自动感知与生命周期绑定)

C++20 的 std::jthread 内部原生集成了 std::stop_source。若可调用对象(Task/Function)的首个参数类型为 std::stop_tokenstd::jthread 会自动传入 token,并在析构时自动触发 request_stop() 并执行 join()

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void worker(std::stop_token stoken) {
    while (!stoken.stop_requested()) {
        std::cout << "Working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }
    std::cout << "Cleaning up...\n";
}

int main() {
    {
        // t 析构时会自动调用 request_stop() 并 join()
        std::jthread t(worker);

        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 亦可显式手动发起:t.request_stop();
    } // 超出作用域,自动发起停止请求并阻塞等待线程结束
    
    return 0;
}

三、 架构亮点与设计哲学

1. 读写分离与权责对等(Separation of Concerns)

接口设计严格区分了"控制端"与"执行端":

  • 控制端(stop_source)独占修改权;
  • 执行端(stop_token)仅保留只读观察权。
    这种编译期强化的类型约束,彻底杜绝了工作线程误触发取消信号的可能性,显著降低了多线程代码的耦合度。

2. 协同式安全与无锁高效设计

  • 协同非强制 :区别于 POSIX pthread_cancel 等强制终止线程的危险做法,该机制强制要求工作线程"主动响应",确保了栈展开(Stack Unwinding)与 RAII 资源释放的完整性。
  • 高效无锁实现 :底层共享状态建立在原子变量与引用计数之上,拷贝与状态检查均无需显式加锁,拥有极高的并发性能。

3. 解决阻塞响应的"灵魂":std::stop_callback

如果仅有轮询(stop_requested()),当线程处于阻塞(如 sleep、网络 I/O、条件变量等待)时将无法及时响应。

  • std::stop_callback 允许向 stop_token 注册回调,一旦 request_stop() 被触发,回调将立即被调用(例如用于中断 Socket 或唤醒条件变量)。
  • C++20 同步引入了 std::condition_variable_any::wait(lock, stoken, pred),彻底解决了"条件变量阻塞等待与取消信号协同"的难题。

四、 局限性与设计遗憾(踩坑避雷指南)

1. 无法直接中断系统级/C API 阻塞

stop_token 本质上是语言层面的状态标记与回调通知机制,无法做到操作系统内核级的信号中断(Interrupted System Call)。

  • 风险点 :若线程阻塞在未封装 stop_callback 的传统 POSIX read() / select() 或 Win32 阻塞 API 中,单纯查询 stop_token 无法唤醒线程。必须在 stop_callback 中手动执行 shutdown(fd) 或关闭 Handle 来强制打断。

2. 回调函数的同步执行上下文与死锁陷阱(极易踩坑)

stop_callback 默认是在调用 request_stop() 的线程上下文中同步(Synchronously)执行的。

  • 死锁风险 :若调用 request_stop() 的线程持有互斥锁 A,而 stop_callback 内部也尝试获取锁 A,将直接引发死锁。
  • 性能抖动 :若注册的回调逻辑较为昂贵,会直接阻塞调用 request_stop() 的主控线程。

3. 缺乏树状/层级化取消支持(Non-Hierarchical)

标准库的实现是扁平化(Flat)的。在复杂的任务调度或 Actor 模型中,常见的"取消父任务自动取消所有子任务,但取消子任务不影响父任务"的层级取消逻辑(Hierarchical Cancellation),C++20 原生机制并不支持,需要结合图/树结构二次封装。

4. 对传统 std::condition_variable 的兼容性限制

由于历史包袱,传统的 std::condition_variable 硬绑定了 std::unique_lock<std::mutex>。出于 ABI 兼容与性能考虑,它无法直接配合 stop_token 使用。必须切换到通用性更强但开销略高的 std::condition_variable_any

相关推荐
fīɡЙtīиɡ ℡1 小时前
深入学习JAVA并发编程(上)
java·开发语言·学习
zzzll11111 小时前
HashMap、HashTable、ConcurrentHashMap 详细区别与深度解析
开发语言·python
老师我太想进步了20262 小时前
C语言版讲解函数递归
c语言·开发语言
猿长大人2 小时前
C# | 函数式编程入门
开发语言·c#·.net
W_326002 小时前
Python 组合数据类型——序列(列表 & 元组)
开发语言·python
Billy121382 小时前
Day10-C++20 Ranges(上):惰性求值与组合式数据处理
开发语言·c++进阶学习
有点。2 小时前
C++深度优先搜索(二)
开发语言·c++·深度优先
二进制杯莫停2 小时前
A和B环境的python版本相同,B环境无法安装pip依赖,离线安装
开发语言·python·pip
l1t3 小时前
kryonix提交的DuckDB 统一并优化标量执行器基础设施 - #24564 PR
开发语言·数据库·数据仓库·sql