找回密码
立即注册
搜索
发回帖 发新帖

6555

积分

0

好友

800

主题
发表于 4 小时前 | 查看: 0| 回复: 0

原定等待一秒,300 毫秒时取消:任务应结束,迟到回调不能再次恢复它。

19.1 关键概念与三个问题

第 18 章由定时回调直接恢复协程。本章引入取消、超时和对象销毁三种结束场景,核心思路是让 WaitState 连接外部回调与协程等待点:等待器登记句柄,回调写入 Reason,协程恢复后取回结果。

概念 含义与本章职责
取消、超时 主动结束等待、等待超过期限
Reason 记录等待状态与结束原因
WaitState 借用句柄,保存 Reason,统一结束等待
SafeTimerAwaiter 登记句柄、取回结果,析构时撤销登记
Task 拥有协程帧,最终 destroy()

下面这张流程图从上往下读。蓝色是已有概念,绿色是本章新增,金色编号对应取消实验的六条日志。

结束等待:首次事件恢复一次,迟到事件直接返回,Task稍后释放帧

WaitState 只借用句柄,Task 拥有帧。 Cancelled 是等待结果,co_return -1 经 promise 保存的 −1 才是任务结果。

需要解决三个问题:

  1. 取消怎样找到等待中的协程?
  2. 怎样只恢复一次并交付结束原因?
  3. 对象或 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。

挂起期间销毁Task:等待器析构调用detach,不恢复协程

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




上一篇:软考高项2027:大模型项目算力成本与Token预算双控
下一篇:Postman 偷改 Claude Code 配置,终端认证报错排查
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-13 16:38 , Processed in 0.901695 second(s), 46 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表