news 2026/5/10 8:34:41

基于开源项目的现代C++工程实践——OnceCallback 实战(四):取消令牌设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于开源项目的现代C++工程实践——OnceCallback 实战(四):取消令牌设计

基于开源项目的现代C++工程实践——OnceCallback 实战(四):取消令牌设计

仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:

https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP

静态网页直接阅览:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/

引言

异步编程里有一个很常见的需求:回调创建之后、执行之前,某个外部条件发生了变化,导致这个回调已经没有意义了——比如回调绑定的对象已经被销毁了,或者任务已经被取消了。这时候我们希望回调在执行前能检查一下"我还该不该执行",而不是傻乎乎地跑一遍。

这就是取消令牌(cancellation token)的用途。这一篇我们来实现一个简化版的取消令牌,然后看它是怎么集成到 OnceCallback 的执行流程中的。

学习目标

  • 理解取消令牌的概念和动机
  • 逐行理解CancelableToken的实现
  • 理解取消机制在impl_run()中的集成方式
  • 理解 void 和非 void 回调在取消时的不同行为

取消令牌的概念

你可以把取消令牌想象成一张"通行证"。创建回调的时候,给回调发一张通行证,通行证上写着"有效"。某个时刻外部条件变化了(比如绑定的对象被销毁),外部代码说"通行证作废了"(调用invalidate())。之后,所有持有这张通行证的回调在执行前检查时都会发现"通行证已经无效",跳过执行。

在 Chromium 里,这个"通行证"就是WeakPtr内部的控制块——WeakPtr指向的对象被销毁后,控制块中的标志位被清除,所有绑定到这个WeakPtr的回调自动取消。我们的简化版不需要WeakPtr那么复杂,只需要一个简单的"有效/无效"标志。

核心需求

取消令牌需要满足三个条件:多个回调可以共享同一个令牌(一个invalidate()让所有回调同时失效)、令牌可以被拷贝和移动(方便在 OnceCallback 内部和外部各持有一份)、失效检查是多线程安全的(外部线程可能在一个线程调用invalidate(),回调在另一个线程检查is_valid())。


CancelableToken 的完整实现

整个取消令牌只有 18 行代码,但每一行都有它的道理。

#pragmaonce#include<atomic>#include<memory>namespacetamcpp::chrome{classCancelableToken{structFlag{std::atomic<bool>valid{true};};std::shared_ptr<Flag>flag_;public:CancelableToken():flag_(std::make_shared<Flag>()){}voidinvalidate(){flag_->valid.store(false,std::memory_order_release);}boolis_valid()const{returnflag_->valid.load(std::memory_order_acquire);}};}// namespace tamcpp::chrome

为什么要用嵌套结构体 Flag

你可能觉得奇怪——为什么不直接在CancelableToken里放一个std::atomic<bool>?原因是shared_ptr管理的是一个堆上的对象。如果直接在CancelableToken里放atomic<bool>shared_ptr管理的是CancelableToken本身——但CancelableToken还有自己的flag_成员,这就变成了shared_ptr<CancelableToken>包含shared_ptr<Flag>的循环。

用嵌套的Flag结构体把需要共享的状态隔离出来,shared_ptr直接管理FlagCancelableToken的拷贝和移动都通过shared_ptr的引用计数自动处理——简洁又正确。另一个好处是Flag结构体方便后续扩展——如果以后需要加更多原子标志(比如取消原因码),直接往Flag里加就行。

shared_ptr 的共享机制

CancelableToken的拷贝构造和拷贝赋值是编译器默认生成的——它做的就是把shared_ptr<Flag>拷贝一份,引用计数 +1。所有通过拷贝创建的令牌副本共享同一个Flag对象。当任何一个副本调用invalidate()时,修改的是同一个Flag::valid,所有副本在下次调用is_valid()时都会看到false

autotoken1=std::make_shared<CancelableToken>();autotoken2=token1;// 共享同一个 Flagtoken1->invalidate();assert(!token2->is_valid());// token2 也看到了失效

memory_order_acquire/release 配对

invalidate()memory_order_release存储falseis_valid()memory_order_acquire加载。这是一对配对的内存序。releasestore 保证了在 store 之前的所有写操作(包括调用invalidate()之前的任何状态修改)对其他线程可见。acquireload 保证了在 load 之后的所有读操作能看到 release store 之前的写入。

在我们的场景里,这意味着如果一个线程调用了invalidate(),另一个线程随后调用is_valid()时一定能看到false——不会有"我刚刚 invalidate 了但 is_valid 还是返回 true"的情况。这是多线程安全的保证。


集成到 OnceCallback

取消令牌通过set_token()方法设置到 OnceCallback 中:

voidset_token(std::shared_ptr<CancelableToken>token){token_=std::move(token);}

token_shared_ptr<CancelableToken>类型,默认是空指针(不启用取消机制)。设置之后,取消令牌的所有权被转移到 OnceCallback 内部。

is_cancelled() 的完整逻辑

[[nodiscard]]boolis_cancelled()constnoexcept{if(status_!=Status::kValid)returntrue;if(token_&&!token_->is_valid())returntrue;returnfalse;}

两层检查。第一层:状态不是 kValid 就返回 true——空回调(kEmpty)和已消费回调(kConsumed)都算"已取消"。这很合理——空回调没东西可执行,已消费回调已经执行过了。第二层:如果有取消令牌且令牌失效了,也返回 true。

impl_run() 中的取消检查

ReturnTypeimpl_run(FuncArgs...args){assert(status_==Status::kValid);// 取消检查在执行前if(token_&&!token_->is_valid()){status_=Status::kConsumed;func_=nullptr;ifconstexpr(std::is_void_v<ReturnType>){return;}else{throwstd::bad_function_call{};}}// 正常消费流程...}

取消检查在执行可调用对象之前进行。如果已取消,直接消费回调但不执行——status_设为 kConsumed,func_置为 nullptr(析构其内部的可调用对象,释放资源)。


void 与非 void 回调的取消行为差异

这里有一个设计决策值得展开讲——void 回调被取消时直接 return(不执行,也不报错),而非 void 回调被取消时抛出std::bad_function_call异常。

原因是调用方的期望不同。void 回调的调用方不期望返回值——调用std::move(cb).run()之后就结束了,不关心回调有没有实际执行。所以被取消的 void 回调直接跳过执行,对调用方是透明的。

非 void 回调的调用方期望拿到返回值——int result = std::move(cb).run()。如果回调被取消了,我们没法提供一个有意义的返回值。返回一个默认值(比如 0)可能掩盖错误——调用方以为回调正常执行了,实际上什么都没做。抛异常虽然看起来激进,但它明确告诉调用方"出了问题",比默默返回错误值更安全。

Chromium 在这里选择直接终止程序(CHECK失败),理由是在 Chrome 的架构中,被取消的回调不应该被调用——调用方应该在调用前检查is_cancelled()。我们选择异常是为了在测试中更容易捕获和验证,而不是直接让程序崩溃。


使用示例

usingnamespacetamcpp::chrome;// 创建令牌和回调autotoken=std::make_shared<CancelableToken>();boolexecuted=false;OnceCallback<void()>cb([&executed]{executed=true;});cb.set_token(token);// 令牌有效时,正常执行assert(!cb.is_cancelled());std::move(cb).run();assert(executed);// 回调被执行了// 创建另一个回调,这次先取消令牌executed=false;autocb2=OnceCallback<void()>([&executed]{executed=true;});cb2.set_token(token);token->invalidate();// 作废令牌assert(cb2.is_cancelled());std::move(cb2).run();// 取消的 void 回调不执行,不抛异常assert(!executed);// 回调没有被执行

注意第二个例子中——cb2.run()调用了,但回调内部的 lambda 没有执行。impl_run()在执行前检查到令牌已失效,直接消费回调并 return。


小结

这一篇我们实现了取消令牌并把它集成到了 OnceCallback 中。CancelableTokenshared_ptr+atomic<bool>实现了轻量级的取消机制——所有令牌副本共享同一个Flag对象,一个invalidate()让所有副本同时失效。集成方式是在impl_run()执行前检查令牌状态——如果已取消,直接消费回调但不执行。void 回调直接 return,非 void 回调抛出std::bad_function_call,这个差异来自调用方对返回值的不同期望。

下一篇我们去看then()链式组合——OnceCallback 四个功能中所有权设计最精巧的一个。

参考资源

  • cppreference: std::shared_ptr
  • cppreference: std::atomic
  • Chromium WeakPtr 文档

相关阅读

  1. OnceCallback 实战(一):动机与接口设计 - 相似度 68%
  2. OnceCallback 实战(二):核心骨架搭建 - 相似度 62%
  3. OnceCallback 实战(三):bind_once 实现 - 相似度 62%
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 8:34:37

ncmdump:网易云音乐NCM加密格式的终极免费解密方案

ncmdump&#xff1a;网易云音乐NCM加密格式的终极免费解密方案 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式音乐文件无法在其他播放器上播放而烦恼吗&#xff1f;ncmdump作为一款专业的NCM格式解密…

作者头像 李华
网站建设 2026/5/10 8:34:08

地震模拟的分治策略与混合谱/时空边界积分框架

1. 地震模拟的计算挑战与分治策略地震与无震滑移&#xff08;SEAS&#xff09;的模拟是理解断层动力学行为的关键技术。传统方法面临两大核心难题&#xff1a;一是弹性波传播导致的复杂应力传递需要高分辨率计算&#xff0c;二是断层网络几何复杂性带来的计算量激增。典型的全动…

作者头像 李华
网站建设 2026/5/10 8:33:09

Nodejs开发者如何快速接入Taotoken大模型聚合平台

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 Node.js 开发者如何快速接入 Taotoken 大模型聚合平台 对于 Node.js 开发者而言&#xff0c;将大模型能力集成到应用中的第一步&am…

作者头像 李华
网站建设 2026/5/10 8:33:06

解锁智能视频剪辑新维度:FunClip如何用AI重塑内容创作工作流

解锁智能视频剪辑新维度&#xff1a;FunClip如何用AI重塑内容创作工作流 【免费下载链接】FunClip Open-source, accurate and easy-to-use video speech recognition & clipping tool, LLM based AI clipping intergrated. 项目地址: https://gitcode.com/GitHub_Trendi…

作者头像 李华
网站建设 2026/5/10 8:32:31

如何轻松解锁QQ音乐加密格式:QMCDecode完整使用指南

如何轻松解锁QQ音乐加密格式&#xff1a;QMCDecode完整使用指南 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认转换…

作者头像 李华
网站建设 2026/5/10 8:31:28

Shipwright:AI编程插件市场,打造专业级AI开发工作流

1. 项目概述&#xff1a;一个为AI编程工具而生的“插件市场” 如果你和我一样&#xff0c;日常开发重度依赖 Cursor、Claude Code 这类 AI 驱动的 IDE&#xff0c;那你肯定遇到过这样的场景&#xff1a;想让 AI 帮你写个单元测试&#xff0c;结果它生成了一堆不痛不痒的断言&a…

作者头像 李华