撤回操作是状态机干预,需为任务定义明确状态枚举并支持补偿动作;用 weak_ptr 管理生命周期,避免阻塞等待,补偿逻辑须显式注册且不可抛异常。

撤回操作本质是状态机干预,不是简单“取消”
异步工作流里所谓“撤回”,不是调用 std::future::cancel()(C++ 标准根本没这玩意),而是主动干预任务生命周期:在任务未执行、执行中、已提交但未完成等不同阶段,按业务语义决定能否撤回、如何清理副作用。关键在于把“可撤回性”设计进状态流转逻辑,而不是事后补救。
- 必须为每个任务节点定义明确的
state枚举(如PENDING、EXECUTING、COMPLETED、CANCELLED、REVERTED) - 撤回请求只能作用于
PENDING或EXECUTING状态;对COMPLETED节点,撤回实际是触发补偿动作(compensating action),比如调用rollback()而非cancel() - 避免用
std::promise直接暴露set_value()—— 一旦设值就不可逆,得包一层带状态检查的safe_set()
用 std::shared_ptr + weak_ptr 控制任务生命周期
裸指针或独占 std::unique_ptr 无法支撑“外部撤回”需求:撤回方需要持有对任务对象的弱引用,而执行方持有强引用。只有当强引用归零时,任务才真正析构;撤回只是让强引用提前释放,并触发清理逻辑。
- 任务类继承自
std::enable_shared_from_this<Task>,对外只传递std::weak_ptr<Task> - 执行线程通过
lock()获取强引用,若返回空则说明已被撤回,直接退出 - 撤回函数内部调用
m_cancel_requested.store(true, std::memory_order_relaxed),但不直接 delete 对象 —— 交给 shared_ptr 自动管理
class Task : public std::enable_shared_from_this<Task> {
public:
void revoke() { m_revoke_requested = true; }
bool is_revoked() const { return m_revoke_requested.load(); }
private:
std::atomic_bool m_revoke_requested{false};
};
补偿逻辑必须显式注册,不能靠 RAII 自动推导
很多人以为在任务析构函数里写 rollback 就够了,但 C++ 异步环境下析构时机不可控:线程可能卡在 I/O、锁等待、或早已 detach,析构根本不会发生。补偿动作必须和正向动作成对注册,并由工作流引擎统一调度。
- 每个任务创建时,必须传入
std::function<void()> compensate,例如数据库事务的ROLLBACK TO SAVEPOINT语句封装 - 引擎在收到撤回请求后,不立即执行 compensate,而是将其作为新任务插入同一线程池(或专用补偿队列),保证顺序和上下文可见性
- 禁止在 compensate 里 throw 异常 —— 补偿失败本身需要可审计日志,而不是导致整个流程崩塌
std::condition_variable 会阻塞撤回响应,改用原子轮询 + 中断点
用 std::condition_variable::wait() 等待子任务完成,会导致撤回请求被延迟数毫秒甚至更久,尤其在线程池负载高时。撤回必须低延迟生效,所以要把“等待”拆成可中断的检查点。
立即学习“C++免费学习笔记(深入)”;
- 执行函数内每处理一段逻辑(如一个 HTTP 请求、一次 DB 查询),就检查
if (task->is_revoked()) { task->compensate(); return; } - 避免长时系统调用阻塞:HTTP 客户端用带超时的
curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, 500),而非无限等待 - 文件读写用非阻塞 I/O 或
std::async包装,主线程不挂起
真正的难点不在怎么发撤回指令,而在怎么定义“这个任务撤回后,整个流程是否还一致”。比如转账流程中,A 扣款成功、B 入账失败,撤回 A 扣款必须确保银行账户余额能精确回滚 —— 这要求每个原子步骤都自带幂等性和版本号,不是靠 C++ 语法能解决的。









