基于开源项目的现代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#includeatomic#includememorynamespacetamcpp::chrome{classCancelableToken{structFlag{std::atomicboolvalid{true};};std::shared_ptrFlagflag_;public:CancelableToken():flag_(std::make_sharedFlag()){}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::atomicbool原因是shared_ptr管理的是一个堆上的对象。如果直接在CancelableToken里放atomicboolshared_ptr管理的是CancelableToken本身——但CancelableToken还有自己的flag_成员这就变成了shared_ptrCancelableToken包含shared_ptrFlag的循环。用嵌套的Flag结构体把需要共享的状态隔离出来shared_ptr直接管理FlagCancelableToken的拷贝和移动都通过shared_ptr的引用计数自动处理——简洁又正确。另一个好处是Flag结构体方便后续扩展——如果以后需要加更多原子标志比如取消原因码直接往Flag里加就行。shared_ptr 的共享机制CancelableToken的拷贝构造和拷贝赋值是编译器默认生成的——它做的就是把shared_ptrFlag拷贝一份引用计数 1。所有通过拷贝创建的令牌副本共享同一个Flag对象。当任何一个副本调用invalidate()时修改的是同一个Flag::valid所有副本在下次调用is_valid()时都会看到false。autotoken1std::make_sharedCancelableToken();autotoken2token1;// 共享同一个 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_ptrCancelableTokentoken){token_std::move(token);}token_是shared_ptrCancelableToken类型默认是空指针不启用取消机制。设置之后取消令牌的所有权被转移到 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_vReturnType){return;}else{throwstd::bad_function_call{};}}// 正常消费流程...}取消检查在执行可调用对象之前进行。如果已取消直接消费回调但不执行——status_设为 kConsumedfunc_置为 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;// 创建令牌和回调autotokenstd::make_sharedCancelableToken();boolexecutedfalse;OnceCallbackvoid()cb([executed]{executedtrue;});cb.set_token(token);// 令牌有效时正常执行assert(!cb.is_cancelled());std::move(cb).run();assert(executed);// 回调被执行了// 创建另一个回调这次先取消令牌executedfalse;autocb2OnceCallbackvoid()([executed]{executedtrue;});cb2.set_token(token);token-invalidate();// 作废令牌assert(cb2.is_cancelled());std::move(cb2).run();// 取消的 void 回调不执行不抛异常assert(!executed);// 回调没有被执行注意第二个例子中——cb2.run()调用了但回调内部的 lambda 没有执行。impl_run()在执行前检查到令牌已失效直接消费回调并 return。小结这一篇我们实现了取消令牌并把它集成到了 OnceCallback 中。CancelableToken用shared_ptratomicbool实现了轻量级的取消机制——所有令牌副本共享同一个Flag对象一个invalidate()让所有副本同时失效。集成方式是在impl_run()执行前检查令牌状态——如果已取消直接消费回调但不执行。void 回调直接 return非 void 回调抛出std::bad_function_call这个差异来自调用方对返回值的不同期望。下一篇我们去看then()链式组合——OnceCallback 四个功能中所有权设计最精巧的一个。参考资源cppreference: std::shared_ptrcppreference: std::atomicChromium WeakPtr 文档相关阅读OnceCallback 实战一动机与接口设计 - 相似度 68%OnceCallback 实战二核心骨架搭建 - 相似度 62%OnceCallback 实战三bind_once 实现 - 相似度 62%