<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】14 取消机制的演进：从手动拼装到隐式注入]]></title><description><![CDATA[<p dir="auto">这是在学习了capy的隐式注入玩法后的更新。他的方案帮我梳理了我原本混乱的思路，感觉自己的目光都清撤了许多。</p>
<hr />
<h2>旧版本的困局：<code>stop_then</code> 手动包装</h2>
<p dir="auto">旧版本就是让用户手工拼装，每个 awaitable 都得用 <code>stop_then</code> 包一下：</p>
<pre><code class="language-cpp">co_await stop_then(some_io_operation(), stop_token);  // 得手动拼，十分繁琐
</code></pre>
<p dir="auto">这种操作确实实现了取消，但是<strong>所有的复杂性都暴露给了业务代码</strong>：</p>
<ol>
<li>编写 <code>co_await</code> 时需要考虑"这个操作是否需要取消？"</li>
<li>如果需要，必须记得手动包裹 <code>stop_then</code>。</li>
<li>遗漏任何一个，就会产生"无法取消的操作"，取消时挂起。</li>
</ol>
<blockquote>
<p dir="auto">值得一提的是，原本的实现由于混乱的思路引入了各种多线程问题。但是在梳理之后发现都是不必要处理的操作，属于是自己把自己坑进去了。</p>
</blockquote>
<hr />
<h2>新版本的改进：隐式注入取消</h2>
<h3>消除业务层污染</h3>
<p dir="auto">旧方案的致命缺陷不在技术复杂度，而在于<strong>污染</strong>。业务代码里到处是 <code>stop_then</code>，每个 I/O 函数签名都要添加 <code>std::stop_token</code> 参数，这直接增加了维护成本和开发者的认知负担。</p>
<pre><code class="language-cpp">// 旧方案：取消把整个调用链都污染了
Task&lt;&gt; handle_client(IOContext&amp; ctx, std::stop_token token) {
    auto res = co_await stop_then(async_read(fd, buffer), token);  // 得手动拼
    if (!res &amp;&amp; res.error() == std::errc::operation_canceled) {
        // 处理被取消
    }
}
</code></pre>
<p dir="auto">我原本认为用户可能需要自己选择可取消和不可取消，所以通过stop_then包装器可以提供最大的自由度。但是实际上，我想不出什么操作是不需要被取消的，甚至更进一步的想，一旦出现了不可取消的挂起操作，某种意义上其实可以算作一种资源泄露bug了。</p>
<blockquote>
<p dir="auto"><strong>想法来源</strong>：从 capy 的实现中得到启发，我发现可以用 C++20 的 <code>await_transform</code> 重新设计这一层。capy 为了通用性，通过 env 注入 executor、allocator、stop_token 等参数；而我们的框架强绑定 io_uring，所以能更直接地在 promise 层注入 IOContext 和 stop_token，并统一依赖 ASYNC_CANCEL 语义。正因为这些都能确定下来，所以取消机制才能被完全自动化。</p>
</blockquote>
<h3>核心改进：三层隐式注入</h3>
<p dir="auto"><strong>第一层：Promise 环境绑定</strong></p>
<p dir="auto">每个 <code>Task</code> 的 promise 自动持有当前的 <code>IOContext</code> 和 <code>stop_token</code>，无需用户处理：</p>
<pre><code class="language-cpp">struct StoppablePromise {
    IOContext&amp; context_;
    std::stop_token stop_token_;
    // ...
};
</code></pre>
<p dir="auto"><strong>第二层：await_transform 统一拦截</strong></p>
<p dir="auto">利用 C++20 的 <code>await_transform</code> 机制，框架可以在业务代码的每一个 <code>co_await IOAwaiter</code> 处进行拦截，自动将其升级为"取消感知"版本：</p>
<pre><code class="language-cpp">//  新模型：零污染，框架自动处理
Task&lt;&gt; handle_client() {
    auto res = co_await async_read(fd, buffer);  // 框架自动注入取消机制
    if (!res &amp;&amp; res.error() == std::errc::operation_canceled) {
        // 处理取消情况
    }
}
</code></pre>
<p dir="auto"><strong>第三层：统一落地语义</strong></p>
<p dir="auto"><code>StopTokenWrapper</code> 在 awaiter 挂起期间隐式注册取消回调。被取消操作的最终结果由原操作的 completion 决定：</p>
<blockquote>
<p dir="auto">也是在重新设计这块时，我发现并不需要处理 ASYNC_CANCEL 的 cqe，直接丢掉，反而让整个实现简单起来了，没有复杂的生命周期问题，也没有了被取消操作和取消操作的时序问题，被 resume 时一定是被取消操作的结果返回了，而且被取消操作的结果也是精确地，result会被正确设置。</p>
</blockquote>
<pre><code class="language-cpp">template&lt;typename Promise&gt;
auto await_suspend(std::coroutine_handle&lt;Promise&gt; handle) noexcept
    -&gt; std::coroutine_handle&lt;&gt;
{
    auto inner_handle = inner_.await_suspend(handle, context());

    // 核心：在挂起期隐式注册取消回调
    if (promise_-&gt;stop_token.stop_possible())
        stop_callback_.emplace(promise_-&gt;stop_token, CancelFn{ this });

    return inner_handle;
}

// 取消触发时的统一路径
void CancelFn::operator()() noexcept {
    if constexpr (cancellable&lt;Awaitable&gt;)
        wrapper-&gt;inner_.cancel(wrapper-&gt;context());  // 若 Awaiter 实现 cancel，直接调用
    else
        wrapper-&gt;context().cancel(wrapper-&gt;inner_);   // 否则走 ASYNC_CANCEL
}
</code></pre>
<h3>修改后</h3>
<ol>
<li>
<p dir="auto"><strong>隐式注入</strong>：用户编写普通的 <code>co_await</code>，框架在后台自动升级为"取消感知"版本。</p>
</li>
<li>
<p dir="auto"><strong>生命周期简化</strong>：由于 cancel CQE 直接扔掉，不再跟踪，<code>stop_callback</code> 的生命周期严格绑定在 awaiter 挂起这段期间。不需要 <code>shared_ptr&lt;bool&gt;</code> 守卫了。</p>
</li>
<li>
<p dir="auto"><strong>awaiter 契约统一</strong>：</p>
<ul>
<li>要是 awaiter 实现了 <code>cancel(io_context)</code> 方法，框架就调它。</li>
<li>否则用默认的 <code>io_uring ASYNC_CANCEL</code> 方式。</li>
</ul>
</li>
</ol>
<h3>核心代码对比</h3>
<p dir="auto">旧版本（用户手动拼）：</p>
<pre><code class="language-cpp">auto task = [](std::stop_token token) -&gt; async::Task&lt;&gt; {
    co_await stop_then(some_io(), token);  // 得手动传 token，手动调用 stop_then
};
co_await async::any(task(group.stop_token()), ...);
</code></pre>
<p dir="auto">新版本（框架自动处理）：</p>
<pre><code class="language-cpp">auto task = []() -&gt; async::Task&lt;&gt; {
    co_await some_io();  // 啥都不用干，框架已经全搞定
};
// stop_token 框架内部在 Task::promise 里已经绑了，用户看不见
</code></pre>
<hr />
<h2>多线程下的行为对比</h2>
<h3>旧版本</h3>
<pre><code>主线程 (IOContext.run())
    ↓
    [处理 I/O 完成事件A]
    ↓
    task 协程被唤醒
    ↓
    stop_then 析构（alive_ = false）
    ↓

后台定时器线程
    ↓
    [发送 stop_token cancel]
    ↓
    post 任务进入队列
    ↓
    [IOContext 取出 post 任务，检查 alive_]
    ↓
    *alive_ == false，安全丢弃
</code></pre>
<p dir="auto">陷阱：如果 post 任务执行前，协程没析构，就访问了已 free 的内存。</p>
<h3>新版本</h3>
<pre><code>主线程 (IOContext.run())
    ↓
    task 协程开始 co_await some_io()
    ↓
    await_transform 自动把它包装成 StopTokenWrapper
    ↓
    stop_callback 被注册在 wrapper 的栈帧上
    ↓

后台定时器线程
    ↓
    [stop_token 被触发]
    ↓
    stop_callback 立刻在这个线程上执行（标准库保证线程安全）
    ↓
    调用 awaiter.cancel(context) 把 cancel 指令 post 回主线程
    ↓

主线程 (IOContext.run())
    ↓
    [从事件循环取出 cancel 指令]
    ↓
    发送 ASYNC_CANCEL SQE 给内核
    ↓
    原操作的 CQE 返回 -ECANCELED 或原结果
    ↓
    awaiter 恢复协程，返回错误给上层
</code></pre>
<p dir="auto">如此一来：</p>
<ul>
<li><code>stop_callback</code> 本身线程安全（标准库保证）。</li>
<li>cancel 动作在 IOContext 里执行，安全、可控。</li>
<li>栈帧生命周期自动保护，不需要 <code>shared_ptr</code> 守卫。</li>
</ul>
<hr />
<h2>结尾</h2>
<p dir="auto">由于这个修改算是撬了最底层的设计，所以导致绝大部分的模块都需要重写。不过在理清了混乱之后，所有的实现相较于旧版本都变得简单了很多，甚至绝大部分的Awaiter实现都变成了下面这种极简版本，也就是继承一下IOAwaiter，写个 prepare()和构造函数就完事了。</p>
<pre><code class="language-c++">class SendAwaiter : public async::IOAwaiter&lt;SendAwaiter, std::size_t&gt; {
private:
    int fd_;
    std::span&lt;const std::byte&gt; buffer_;

public:
    SendAwaiter(int fd, std::span&lt;const std::byte&gt; buffer)
      : fd_{ fd }
      , buffer_{ buffer }
    {}

    void prepare(::io_uring_sqe* sqe) const noexcept
    {
        ::io_uring_prep_send(sqe, fd_, buffer_.data(), buffer_.size(), MSG_NOSIGNAL);
    }
};
</code></pre>
]]></description><link>http://forum.d2learn.org/topic/208/基于-io_uring-的-c-20-协程网络库-14-取消机制的演进-从手动拼装到隐式注入</link><generator>RSS for Node</generator><lastBuildDate>Mon, 18 May 2026 05:16:31 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/topic/208.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 17 May 2026 17:24:34 GMT</pubDate><ttl>60</ttl></channel></rss>