原定等待一秒,300 毫秒时取消:任务应结束,迟到回调不能再次恢复它。
19.1 关键概念与三个问题
第 18 章由定时回调直接恢复协程。本章引入取消、超时和对象销毁三种结束场景,核心思路是让 WaitState 连接外部回调与协程等待点:等待器登记句柄,回调写入 Reason,协程恢复后取回结果。
| 概念 |
含义与本章职责 |
| 取消、超时 |
主动结束等待、等待超过期限 |
| Reason |
记录等待状态与结束原因 |
| WaitState |
借用句柄,保存 Reason,统一结束等待 |
| SafeTimerAwaiter |
登记句柄、取回结果,析构时撤销登记 |
| Task |
拥有协程帧,最终 destroy() |
下面这张流程图从上往下读。蓝色是已有概念,绿色是本章新增,金色编号对应取消实验的六条日志。

WaitState 只借用句柄,Task 拥有帧。 Cancelled 是等待结果,co_return -1 经 promise 保存的 −1 才是任务结果。
需要解决三个问题:
- 取消怎样找到等待中的协程?
- 怎样只恢复一次并交付结束原因?
- 对象或 Task 提前销毁怎么办?
全程同一线程,无嵌套事件循环。WaitState 只服务一次等待,并且比 Task 活得久。
19.2 先运行一个取消实验
先预测:取消后还会执行成功分支或再次恢复吗?
.\chapter19.exe cancel
工作定时器 1000 毫秒,期限 1200 毫秒。300 毫秒取消,1400 毫秒送入一次迟到的完成通知,约 1600 毫秒结束。
输出六条日志:
[main] CANCEL_REQUEST
[WaitState::finish] FINISH Cancelled
[WaitState::finish] RESUME_ONCE
[runJob] STOP Cancelled
[WaitState::finish] IGNORE_LATE_EVENT
[Task::reset] DESTROY_FRAME
方括号里是打印这条日志的函数。取消路径不会进入成功分支。下面源码中的注释对应这六条日志。
main.cpp:请求取消与正文收尾
// main() 中的300毫秒取消回调
QTimer::singleShot(300, &state, [&state] {
trace("main", "CANCEL_REQUEST"); // 第1条:请求取消
state.finish(Reason::Cancelled);
});
Task<int> runJob(WaitState& state) {
const auto reason = co_await SafeTimerAwaiter{state};
if (reason != Reason::Completed) {
// 第4条:本次输出 [runJob] STOP Cancelled
traceReason("runJob", "STOP", reasonName(reason));
co_return -1;
}
trace("runJob", "AFTER_WAIT_SUCCESS"); // 本次取消不走成功分支
co_return 42;
}
SafeTimerAwaiter.h:WaitState::finish() 统一处理结束事件
void finish(Reason reason) {
if (reason_ != Reason::Waiting) {
trace("WaitState::finish", "IGNORE_LATE_EVENT"); // 第5条:迟到通知在此返回
return; // 退出本次 finish,保留首次结束原因
}
reason_ = reason;
// 第2条:本次输出 [WaitState::finish] FINISH Cancelled
traceReason("WaitState::finish", "FINISH", reasonName(reason));
stopSources();
auto next = std::exchange(handle_, {});
if (next) {
trace("WaitState::finish", "RESUME_ONCE"); // 第3条:恢复一次
next.resume();
}
// resume 可能触发 awaiter 析构;此后不再访问状态或句柄。
}
return 只退出本次 finish(),不改写已经记下的原因。 清空句柄用来防止再次恢复。
Task.h:析构时通过 reset() 释放帧
~Task() { reset(); }
void reset() noexcept {
if (auto h = std::exchange(handle_, {})) {
trace("Task::reset", "DESTROY_FRAME"); // 第6条:即将释放帧
h.destroy();
}
}
19.3 问题一 取消怎样找到等待中的协程
解决办法:等待器和回调访问同一个 WaitState。
task.resume() 执行到 co_await。原因仍是 Waiting,await_ready() 返回 false。编译器把当前协程的句柄交给等待器,start() 把它存进 state,并启动工作和期限两个定时器。resume() 随后回到 main。main 注册 300 毫秒回调,进入 app.exec()。回调打印第1条日志,调用 state.finish(Reason::Cancelled)。
// SafeTimerAwaiter:向同一个 WaitState 登记句柄
void await_suspend(std::coroutine_handle<> h) const { state.start(h); }
// WaitState:保存句柄,启动两个定时器
void start(std::coroutine_handle<> h) {
handle_ = h;
timer_.start(workMs_);
deadline_.start(timeoutMs_);
}
等待器持有的引用和回调捕获的 &state 是同一个对象:前者登记句柄,后者凭它找到协程。
19.4 问题二 怎样只恢复一次并交付结束原因
怎样只恢复一次并交付结束原因?解决办法是:所有结束事件都进入 finish(),只有第一次生效。
首次进入时,reason_ 从 Waiting 改为 Cancelled,打印第2条日志。stopSources() 停止定时器并断开连接。std::exchange 取走句柄,打印第3条日志,再 next.resume()。这次恢复是同步的。await_resume() 从同一个 state 读出原因:
Reason await_resume() const noexcept {
return state.reason();
}
co_await 得到 Cancelled,runJob 打印第4条日志并 co_return -1。resume() 返回之后,finish() 不再访问等待状态。协程在 final_suspend() 挂起,调用栈已经退回,帧仍由 Task 持有。
结束原因就是 co_await 的结果。−1 和 42 是本例的约定,Task 不必另设取消接口。正常完成、超时也走 finish(),分别交付 Completed、TimedOut。
1400 毫秒再次调用 finish(Completed) 时,原因已经不是 Waiting,函数直接返回,打印第5条日志。断开连接之后,已经排队的槽仍可能到达,所以还要靠这次状态检查。
main 离开作用域时,reset() 打印第6条日志并销毁帧。
19.5 问题三 对象或 Task 提前销毁怎么办
解决办法:对象销毁交付结束原因,Task 提前销毁则撤销登记。
被观察对象和 WaitState 分开。销毁信号使用队列连接,避开对象自己的析构调用栈。恢复后只做收尾,不再访问该对象:
targetGone_ = connect(&target, &QObject::destroyed, this, [this] {
finish(Reason::TargetGone);
}, Qt::QueuedConnection);
挂起期间销毁 Task,会析构帧里的等待器:
~SafeTimerAwaiter() { state.detach(); }
detach() 把 Waiting 改为 Abandoned,清空句柄并停止定时器,不恢复协程。后面的迟到事件仍由状态检查挡住,因此 WaitState 必须比 Task 活得久。main 先创建 state、后创建 task,离开作用域时先销毁 task。

停止定时器不会使协程继续执行。取消由 finish() 收尾,析构只负责撤销登记。以上便是 C++ 协程等待期间安全结束任务的完整机制,更多深入讨论可以关注云栈社区的后续内容。