<?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[Topics tagged with c++20协程]]></title><description><![CDATA[A list of topics that have been tagged with c++20协程]]></description><link>http://forum.d2learn.org/tags/c++20协程</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Jul 2026 22:34:39 GMT</lastBuildDate><atom:link href="http://forum.d2learn.org/tags/c++20协程.rss" rel="self" type="application/rss+xml"/><pubDate>Invalid Date</pubDate><ttl>60</ttl><item><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><guid isPermaLink="true">http://forum.d2learn.org/topic/208/基于-io_uring-的-c-20-协程网络库-14-取消机制的演进-从手动拼装到隐式注入</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】13 实现取消机制]]></title><description><![CDATA[<p dir="auto">@SPeak 视频不考虑做了，整个系列算是我留给自己看的复盘。还会补一个await_transform的思路。如果没有别的新思路整个系列就结束了。以后最可能的是在这个基础上做http或者别的应用了</p>
]]></description><link>http://forum.d2learn.org/topic/206/基于-io_uring-的-c-20-协程网络库-13-实现取消机制</link><guid isPermaLink="true">http://forum.d2learn.org/topic/206/基于-io_uring-的-c-20-协程网络库-13-实现取消机制</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】12 零拷贝发送SEND_ZC]]></title><description><![CDATA[<p dir="auto">经过了大量的缝缝补补之后，现在已经是一个初步可用状态了。某些细节和博客中的内容有些对不上，但是大体思路是一致的。构建需要cmake 和vcpkg，感兴趣的同学可以自己clone下来玩：https://github.com/Doomjustin/blog.git</p>
]]></description><link>http://forum.d2learn.org/topic/201/基于-io_uring-的-c-20-协程网络库-12-零拷贝发送send_zc</link><guid isPermaLink="true">http://forum.d2learn.org/topic/201/基于-io_uring-的-c-20-协程网络库-12-零拷贝发送send_zc</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】11 停止机制补完]]></title><description><![CDATA[<p dir="auto">在第 01 篇里，我留了两个 stop 相关的问题。</p>
<p dir="auto">第一个是：<code>stop()</code> 调了，但 <code>run()</code> 可能还卡在 <code>submit_and_wait</code> 里出不来。<br />
第二个是：就算 <code>run()</code> 退了，内核里可能还有已提交但未完成的操作。</p>
<p dir="auto">第一个问题在第 02 篇已经用 <code>eventfd</code> 解决了。这篇只聊第二个：怎么把停机过程做成可验证的<strong>先取消、再排干、最后退出</strong>。</p>
<h3>1. 强行退出的Pending状态与资源泄漏隐患</h3>
<p dir="auto">目前 <code>run()</code> 的退出条件为：</p>
<pre><code class="language-cpp">while (!should_stop_.load(std::memory_order_relaxed) &amp;&amp; outstanding_works_ &gt; 0)
</code></pre>
<p dir="auto">当 <code>should_stop_</code> 置为 true 时，循环会立刻终止。但这时协程可能还挂在 <code>read</code>、<code>accept</code>、<code>sleep_for</code> 上，对应 SQE 已交给内核，CQE 还没回来。</p>
<p dir="auto">如果事件循环直接退出，<code>io_uring</code> 被析构后，用户态后续完成路径就可能拿到历史的 <code>user_data</code> 并把它当作 <code>Operation*</code> 解引用。只要这个对象已经销毁，就会变成悬垂指针风险。靠<strong>析构顺序刚好没出事</strong>不是一个可靠方案。</p>
<h3>2. 确定的停机序列：取消与排干（Draining）</h3>
<p dir="auto">正确的回收顺序是：退出前先把挂起操作都收回来。</p>
<p dir="auto">所以 <code>stop()</code> 触发后，不应该立刻跳出循环，而是要先向内核发取消请求，再等所有操作（包括被取消的操作）把 CQE 回完，最后再退出。</p>
<p dir="auto"><code>io_uring</code> 提供了 <code>io_uring_prep_cancel</code> 接口。通过传入 <code>Operation*</code> 作为 <code>user_data</code>，内核能够定位并中断对应的操作。随后，内核会投递一个结果为 <code>-ECANCELED</code> 的 CQE，触发该操作的 <code>complete()</code> 回调，使得挂起的协程能够沿着正常的异常或错误处理路径恢复。</p>
<p dir="auto">这样做的好处是，不管停机发生在什么时候，协程都还能沿正常路径恢复，栈帧能按预期展开和销毁。前提是：我们得准确知道当前还有多少活跃操作。</p>
<h3>3. 方案取舍：全局取消的局限性与侵入式追踪</h3>
<p dir="auto">有一个更省代码的做法：提交一个特殊 Cancel SQE，带上 <code>IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_ALL</code>，让内核尽可能全量取消。</p>
<p dir="auto">这条路看起来很短，但我最后没选，原因有三个。</p>
<p dir="auto">第一，状态不够确定。它是“尽力取消”，不是“给我精确活跃列表”。命令发出去后，用户态依然不知道还有多少 CQE 没回来。</p>
<p dir="auto">第二，语义太粗。像 <code>timeout</code> 组合子这种场景，一个 <code>co_await</code> 可能拆成多个底层请求（业务 I/O + timer）。全量取消能快速收敛，但会把上层状态机语义一起打平。</p>
<p dir="auto">第三，也是最关键的：我真正要解决的是“<code>run()</code> 什么时候能安全退出”。没有精确活跃计数，退出条件就没法验证。</p>
<p dir="auto">tips：</p>
<ol>
<li>这类 ANY/ALL 取消属于能力驱动特性，使用前要确认内核与 liburing 版本是否支持对应标志位语义。</li>
<li>取消结果本身也会通过 CQE 回来，可能出现“目标已完成/未命中”等结果码，这些都意味着它不适合作为唯一退出判据。</li>
<li>在我的实现里，它可以作为“加速收敛”的辅助工具，但不能替代用户态的活跃操作追踪。</li>
</ol>
<p dir="auto">所以我还是选了侵入式链表。链表不空，说明还有操作在等 CQE；链表清空，才允许退出。</p>
<p dir="auto">每个 awaiter 本来就继承自 <code>Operation</code>，把 <code>prev/next</code> 放进基类，插入删除都是 O(1)，而且不需要额外分配。</p>
<h3>4. 零开销追踪：侵入式链表实现</h3>
<p dir="auto">实现上我做得很直接：把侵入式指针放进 <code>Operation</code> 基类。</p>
<pre><code class="language-cpp">struct Operation {
    Operation* prev{ nullptr };
    Operation* next{ nullptr };
    bool is_canceling_{ false };

    virtual ~Operation() = default;
    virtual void complete(int res, std::uint32_t flags) noexcept = 0;
};
</code></pre>
<p dir="auto"><code>IOContext</code> 中维护链表指针及活跃操作计数：</p>
<pre><code class="language-cpp">Operation* head_{ nullptr };
Operation* tail_{ nullptr };
std::size_t tracking_operations_{ 0 };
</code></pre>
<p dir="auto"><code>track()</code> 和 <code>untrack()</code> 分别负责挂入、摘除，同时更新计数：</p>
<pre><code class="language-cpp">void IOContext::track(gsl::not_null&lt;Operation*&gt; operation) noexcept
{
    if (!head_) {
        head_ = tail_ = operation;
    }
    else {
        tail_-&gt;next = operation;
        operation-&gt;prev = tail_;
        tail_ = operation;
    }
    add_work();
}

void IOContext::untrack(gsl::not_null&lt;Operation*&gt; operation) noexcept
{
    // ... 链表节点摘除逻辑 ...
    operation-&gt;prev = operation-&gt;next = nullptr;
    drop_work();
}
</code></pre>
<h3>5. 生命周期绑定：在 Awaiter 中管理注册</h3>
<p dir="auto">以 <code>ReadSomeAwaiter</code> 为例，我这里遵循一个很朴素的约定：<code>await_suspend</code> 里注册，<code>complete</code> 里注销。</p>
<pre><code class="language-cpp">void ReadSomeAwaiter::await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
{
    handle_ = handle;

    auto* sqe = context_.sqe();
    prepare(sqe);
    ::io_uring_sqe_set_data(sqe, this);

    context().track(this);   // 提交 SQE 后显式声明生命周期开始
}

void ReadSomeAwaiter::complete(int result, std::uint32_t flags) noexcept
{
    context().untrack(this);  // CQE 返回后第一优先注销生命周期

    set_result(result, flags);
    if (handle_) {
        handle_.resume();
    }
}
</code></pre>
<p dir="auto">其他底层 awaiter 也都按这个模式来，这样挂起操作和链表节点始终是一对一关系。</p>
<p dir="auto">写路径里有个值得单独提一下的特例：<code>WriteAllAwaiter</code>。</p>
<p dir="auto">它和 <code>WriteSomeAwaiter</code> 不一样，不是一次 SQE 就结束，而是“分段发送直到 buffer 清空”的循环模型。也正因为这样，<code>track/untrack</code> 的时机要更小心：每一轮 <code>complete()</code> 先 <code>untrack(this)</code>，再决定是 <code>resume</code> 还是继续 <code>arm_write()</code>。</p>
<pre><code class="language-cpp">void WriteAllAwaiter::complete(int result, std::uint32_t flags) noexcept
{
    context_.untrack(this);
    set_result(result, flags);

    if (is_canceling_ || error_code_ != 0 || buffer_.empty()) {
        if (is_canceling_ &amp;&amp; error_code_ == 0)
            error_code_ = ECANCELED;
        resume(handle_, result, flags);
    }
    else {
        arm_write();
    }
}
</code></pre>
<p dir="auto">这里还有一个细节：取消路径里，如果当前轮没有带出错误码，会主动归一到 <code>ECANCELED</code>。这样上层就不会看到“已经取消但结果像成功”的歧义状态。</p>
<h3>6. 复杂状态机处理：超时组合子与 <code>sqe()</code> 接口收敛</h3>
<p dir="auto">普通 awaiter 基本是一对一：一个 <code>Operation</code> 对一个 SQE。到了 timeout 组合逻辑，就会出现多 CQE 的竞争。</p>
<p dir="auto">第一种是 <code>TimeoutAwaiter</code>。它用 <code>IOSQE_IO_LINK</code> 把业务操作和超时请求绑在一起。虽然内核会回两个 CQE，但它们共享同一个 <code>Operation</code> 节点，所以只需要追踪一次，等两个 CQE 都回来再摘除：</p>
<pre><code class="language-cpp">void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
{
    // 发送两个 linked SQE，但在业务视角仅追踪一个逻辑单元
    context().track(this);
}

void complete(int result, std::uint32_t flags) noexcept override
{
    set_result(result, flags);

    if (--pending_cqes_ == 0) {
        context().untrack(this);  // 确保底层事件全部终结
        handle_.resume();
    }
}
</code></pre>
<p dir="auto">第二种是 <code>TimeoutCombinator</code>。这条路是解耦设计：业务操作和计时器作为两个独立 <code>Operation</code> 提交。</p>
<pre><code class="language-cpp">void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
{
    auto* sqe = context().sqe();
    ::io_uring_prep_timeout(sqe, &amp;timeout_, 0, 0);
    ::io_uring_sqe_set_data(sqe, &amp;timer_);

    context().track(&amp;timer_);        // 追踪独立的计时器节点
    awaiter_.await_suspend(handle);  // 内部 Awaiter 自行管理追踪
}
</code></pre>
<p dir="auto">在竞速过程中，谁先完成，就调用 <code>context().cancel()</code> 去中断另一方。这样 timeout 组合子和全局停机路径就复用了同一套取消逻辑，也把旧版 <code>sqe(false)</code> 这种隐式开关顺带收掉了。</p>
<p dir="auto">这其实是一个我早就想动的点了：<code>sqe(bool tracking = true)</code>。</p>
<p dir="auto">这个参数最初是我为了赶进度加的。当时想法很直接：业务请求默认 <code>sqe(true)</code>，取消请求写 <code>sqe(false)</code>，先把功能跑通。</p>
<p dir="auto">但写完我就觉得不舒服。调用点里一个裸 <code>bool</code>，语义全靠人脑补，<code>false</code> 到底是“不计数”还是“这是取消请求”，每次都得回到定义处确认。这是一种典型的bad smell了。</p>
<p dir="auto">旧接口是这样的：</p>
<pre><code class="language-cpp">auto sqe(bool tracking = true) -&gt; ::io_uring_sqe*;

// 取消路径
auto* sqe = context().sqe(false);
::io_uring_prep_cancel(sqe, &amp;timer_, 0);
::io_uring_sqe_set_data(sqe, nullptr);
</code></pre>
<p dir="auto">这次停机重构正好给了我一个机会，把这个开关彻底拿掉：<code>sqe()</code> 只负责“拿一个 SQE”；计数交给 <code>track/untrack</code>；取消统一走 <code>cancel()</code>。</p>
<pre><code class="language-cpp">auto sqe() -&gt; ::io_uring_sqe*;

// 取消路径
context().cancel(&amp;timer_);
</code></pre>
<p dir="auto">拆完之后，接口职责就清楚了：</p>
<ol>
<li><code>sqe()</code> 只做资源分配。</li>
<li><code>track/untrack</code> 只做生命周期计数。</li>
<li><code>cancel()</code> 统一承载取消语义。</li>
</ol>
<p dir="auto">所以这次看起来是在修停机，其实也顺手把一处历史包袱清掉了。<code>sqe(bool)</code> 这个临时方案当时确实帮我快速推进了实现，但到了这个阶段，是时候干掉他了。</p>
<h3>7. 重构事件循环：排干后退出</h3>
<p dir="auto">事件循环的退出条件现在就一条：活跃计数归零。</p>
<pre><code class="language-cpp">void IOContext::run()
{
    while (tracking_operations_ &gt; 0) {
        if (should_stop_.load(std::memory_order_relaxed)) {
            auto* current = head_;
            while (current) {
                cancel(current);
                current = current-&gt;next;
            }
        }

        scheduler_.schedule();
    }
}
</code></pre>
<p dir="auto"><code>cancel()</code> 负责防重复，并投递取消指令：</p>
<pre><code class="language-cpp">void IOContext::cancel(gsl::not_null&lt;Operation*&gt; operation) noexcept
{
    if (operation-&gt;is_canceling_) return;  

    if (auto* sqe = scheduler_.sqe()) {
        ::io_uring_prep_cancel(sqe, operation, 0);
        ::io_uring_sqe_set_data(sqe, nullptr);  // 显式丢弃取消动作本身的 CQE
        operation-&gt;is_canceling_ = true;
    }
}
</code></pre>
<p dir="auto">到这里，<code>schedule()</code> 就不需要再返回“这一轮完成了多少”。计数和生命周期都在 <code>Operation</code> 自己的 <code>track/untrack</code> 路径里闭环。</p>
<h3>8. 完整的停机时序</h3>
<p dir="auto">停机流程如下：</p>
<pre><code class="language-text">[async::stop() 触发]
      |
      | 置 should_stop_ = true
      | eventfd 写入信号，中断 submit_and_wait 阻塞
      v
[run() 进入排干模式]
      |
      | 遍历侵入式链表
      | 调用 io_uring_prep_cancel 派发中断指令
      v
[内核态收割]
      |
      | 中止 I/O 轮询
      | 投递包含 -ECANCELED 的 CQE
      v
[schedule() 消费 CQE]
      |
      | 触发 op-&gt;complete(-ECANCELED, 0)
      | 节点自我 untrack()，递减 tracking_operations_
      | 协程带着 operation_canceled 错误流转并销毁
      v
[tracking_operations_ 归零]
      |
      | run() 循环安全终止
      v
[资源清理]
</code></pre>
<p dir="auto">通过这套机制，停机路径就变成了可验证流程：先唤醒、再取消、再排干、最后退出。在当前实现约束下，这能持续收敛悬垂指针风险，也让退出行为更可预期。</p>
]]></description><link>http://forum.d2learn.org/topic/200/基于-io_uring-的-c-20-协程网络库-11-停止机制补完</link><guid isPermaLink="true">http://forum.d2learn.org/topic/200/基于-io_uring-的-c-20-协程网络库-11-停止机制补完</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】10 API重构：让用户不再关心IOContext]]></title><description><![CDATA[<p dir="auto">@SPeak 以后可能会独立出一个用module实现的库</p>
]]></description><link>http://forum.d2learn.org/topic/199/基于-io_uring-的-c-20-协程网络库-10-api重构-让用户不再关心iocontext</link><guid isPermaLink="true">http://forum.d2learn.org/topic/199/基于-io_uring-的-c-20-协程网络库-10-api重构-让用户不再关心iocontext</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】09 读路径优化：recv_multishot与ReceiveStream]]></title><description><![CDATA[<p dir="auto">在上一篇里，我们解决了"怎么把数据发出去"。这一篇转到读路径：把 <code>recv_multishot</code> 接进来，减少高频读取时的重复提交。</p>
<p dir="auto">如果沿用 <code>async_read_some</code>，每次读取都要先准备一块可写 buffer，再提交一次 <code>recv</code>，等 CQE 回来后再决定要不要继续下一次。这个模型本身没错，但放到 Echo、代理、网关这类长连接里，会很快变成重复劳动：准备 buffer、提交 SQE、等待 CQE、再补上下一次读取。</p>
<p dir="auto"><code>recv_multishot</code> 能直接解决这件事：一次提交，对应后续多次完成。</p>
<p dir="auto">在开始之前，先确认一下当前库的状态。前几篇一路写下来，<code>IOContext</code> 一直是个比较扁平的类：持有一个 <code>io_uring</code> ring 句柄，提供 SQE 获取和事件循环驱动，加上 work 计数和 stop 信号：</p>
<pre><code class="language-cpp">class IOContext {
    ::io_uring ring_;
    int event_fd_;
    std::size_t outstanding_works_{ 0 };
    std::atomic&lt;bool&gt; should_stop_{ false };

    void run();
    auto sqe() -&gt; ::io_uring_sqe*;
    void wakeup();
    // CQE 分发 ...
};
</code></pre>
<p dir="auto">socket 层的每个异步操作，把自己包装成 <code>Operation</code> 提交进 ring，CQE 回来时由 <code>IOContext</code> 分发回去。至于 buffer，一直是由调用方临时传入，操作完成后调用方自己处理生命周期。</p>
<p dir="auto">这个结构目前是干净的，但 <code>recv_multishot</code> 要求的东西比这多一层。</p>
<hr />
<h3>1. 直接加进去会碰到什么</h3>
<p dir="auto"><code>recv_multishot</code> 需要 buffer ring：内核不再接受调用方临时传入的单块 buffer，而是要求预先注册一组固定大小的槽位。每次有数据到达，内核从 ring 里借一个槽位写入，CQE 里带上这次借出的槽位编号，调用方读完数据后再把槽位还回去。</p>
<p dir="auto">最直接的想法是往 <code>IOContext</code> 里加几个字段：</p>
<pre><code class="language-cpp">class IOContext {
    ::io_uring ring_;
    int event_fd_;
    std::size_t outstanding_works_{ 0 };
    std::atomic&lt;bool&gt; should_stop_{ false };

    // 新加的 buffer ring 管理
    std::unordered_map&lt;unsigned, BufferRing&gt; buffer_rings_;
    unsigned next_bgid_{ 0 };
    std::optional&lt;unsigned&gt; default_bgid_;
};
</code></pre>
<p dir="auto">功能上能跑，但麻烦随之而来。</p>
<p dir="auto">第一，<code>IOContext</code> 现在要同时处理两类完全不同的事情：一类是"这一次 submit_and_wait 怎么跑、CQE 怎么分发"，另一类是"有哪些 buffer 组被注册在内核里、槽位怎么借出和归还"。这两件事没有天然的耦合关系，混在一起只会让两侧的逻辑都难以单独修改。</p>
<p dir="auto">第二，更直接的问题出在 CQE 分发上。之前"一个 CQE 对应一次操作完成"的判断，在 multishot 里不再成立——事件循环必须识别 <code>IORING_CQE_F_MORE</code>，在这个 flag 还存在时不能把这笔 work 从计数里扣掉。这不是细节调整，而是分发逻辑本身语义的变化。如果这块逻辑和 buffer ring 管理搅在同一段代码里，两边会互相干扰。</p>
<p dir="auto">第三，socket 层如果要用 buffer ring，就必须拿到 <code>bgid</code> 和 <code>bid</code>，然后到处传。调用方写业务代码时不该感知这些细节，但没有合适的封装层，这些细节就只能往上漏。</p>
<p dir="auto">所以在写 multishot awaiter 之前，要先把 <code>IOContext</code> 的职责拆开：</p>
<ol>
<li>和 ring 驱动、唤醒、CQE 分发有关的逻辑，收进独立的 <code>Scheduler</code>。</li>
<li>buffer ring 的建立、槽位借出和归还，收进独立的 <code>BufferRingGroup</code>。</li>
<li><code>IOContext</code> 自己只保留对外协调接口和 work tracking。</li>
<li>socket 层只暴露消费接口，不让业务层直接碰 buffer slot。</li>
</ol>
<p dir="auto">这不是为了"代码好看"，而是为了把复杂度放回正确位置。不做这一步，后面无论接收流、buffer 池复用还是取消清理，都会越来越难理顺。</p>
<hr />
<h3>2. 重构后的 <code>IOContext</code></h3>
<p dir="auto">拆分后的 <code>IOContext</code> 把两类职责分别交给两个内部类：</p>
<pre><code class="language-cpp">class IOContext {
private:
    class Scheduler {
        // ring 初始化、SQE 获取、submit_and_wait、wakeup
    };

    class BufferRingGroup {
        // setup buffer ring、release buffer slot、default bgid
    };

    Scheduler scheduler_;
    BufferRingGroup buffers_;
    std::size_t outstanding_works_{ 0 };
    std::atomic&lt;bool&gt; should_stop_{ false };
};
</code></pre>
<p dir="auto"><code>Scheduler</code> 和 <code>BufferRingGroup</code> 的职责本来就不是一回事。</p>
<p dir="auto"><code>Scheduler</code> 关注的是"这一次事件循环怎么跑"：</p>
<ol>
<li>初始化和销毁 <code>io_uring</code> ring。</li>
<li>提供 SQE。</li>
<li><code>submit_and_wait</code> 后遍历 CQE，分发给对应 <code>Operation</code>。</li>
<li>通过 <code>eventfd</code> 做跨线程 stop 唤醒。</li>
</ol>
<p dir="auto">尤其是第 3 点，在引入 multishot 之后已经不能再按"一个 CQE 对应一个完整操作"来理解了。<code>Scheduler::schedule()</code> 现在必须识别 <code>IORING_CQE_F_MORE</code>，只有在 multishot 真正结束时才能把这笔 work 从计数里扣掉。这意味着 <code>recv_multishot</code> 不只是给 socket 层加了个新能力，它也顺手改写了事件循环对"完成"这件事的理解。</p>
<p dir="auto">而 <code>BufferRingGroup</code> 关心的是另一件事："有哪些 buffer 组被长期注册在内核里，以及它们怎么被重复借出和归还"。这部分如果混进 <code>Scheduler</code>，后者就会一边跑 CQE 批调度，一边操心 buffer 池资源生命周期，边界很快就会糊掉。</p>
<p dir="auto">接口本身也能看出这种分层：</p>
<pre><code class="language-cpp">auto setup_buffer_ring(unsigned entries, unsigned size) -&gt; unsigned
{
    return buffers_.setup(scheduler_.ring(), entries, size);
}

void release_buffer_ring(unsigned bgid, unsigned bid)
{
    buffers_.release(bgid, bid);
}
</code></pre>
<p dir="auto"><code>IOContext</code> 在这里做的事情很克制：buffer ring 的建立确实需要底层 ring 句柄，但建立完成之后，后续使用者不必再感知这些细节。</p>
<p dir="auto">有了这层结构，<code>ReceiveStream</code> 才真正有了落脚点。否则它一边要操心 multishot 请求，一边还得自己解决 buffer group 的分配和归还，那就不是 socket 接口该承担的事情。</p>
<p dir="auto">它在 <code>StreamSocket</code> 上的入口很轻：</p>
<pre><code class="language-cpp">auto receive_stream() -&gt; ReceiveStream&lt;Context&gt;
{
    auto default_bgid = this-&gt;context().default_buffer();
    if (!default_bgid)
        throw std::runtime_error{ "No default buffer ring available for receive stream" };

    return ReceiveStream&lt;Context&gt;{ this-&gt;context(), this-&gt;native_handle(), *default_bgid };
}
</code></pre>
<p dir="auto"><code>ReceiveStream</code> 不是独立工作的，它默认依赖 <code>IOContext</code> 里已经准备好的 buffer ring。在当前实现里，第一次 <code>setup_buffer_ring()</code> 创建出来的组会自动成为默认组，所以 demo 里只做一次初始化就够了。</p>
<hr />
<h3>3. 重构落地：把 buffer ring 收进 <code>IOContext</code></h3>
<p dir="auto">buffer ring 进入 <code>IOContext</code> 之后，下一步才轮到它自己的实现。</p>
<p dir="auto"><code>BufferRingGroup::setup()</code> 做了三件事：分配一整块连续内存、向内核注册一个 buf ring、把每个 slot 预先填进 ring。</p>
<pre><code class="language-cpp">auto IOContext::BufferRingGroup::setup(::io_uring* ring, unsigned entries, unsigned size) -&gt; unsigned
{
    auto bgid = next_bgid_++;
    auto&amp; buffer_ring = group_[bgid];

    buffer_ring.size = size;
    buffer_ring.entries = entries;
    buffer_ring.mask = ::io_uring_buf_ring_mask(entries);

    const auto alloc_size = static_cast&lt;std::size_t&gt;(entries * size);
    buffer_ring.base_address = memory_resource_-&gt;allocate(alloc_size, ALIGNMENT);

    int res = 0;
    buffer_ring.buffer = ::io_uring_setup_buf_ring(ring, entries, bgid, 0, &amp;res);

    auto* base = static_cast&lt;std::byte*&gt;(buffer_ring.base_address);
    for (unsigned i = 0; i &lt; entries; ++i)
        ::io_uring_buf_ring_add(buffer_ring.buffer, base + i * size, size, i, buffer_ring.mask, i);

    ::io_uring_buf_ring_advance(buffer_ring.buffer, entries);
    buffer_ring.tail = entries;

    if (!default_buffer_bgid_)
        default_buffer_bgid_ = bgid;

    return bgid;
}
</code></pre>
<p dir="auto">这部分有三个会直接影响行为的选择。</p>
<p dir="auto">第一，所有 buffer slot 放在一整块连续内存里，而不是单独 <code>new</code> 一堆小块。这么做不是图省事，而是因为 buf ring 的典型使用方式本来就是"固定大小、重复借用、按槽位编号回收"。连续布局让 <code>bid -&gt; 地址</code> 的映射退化成简单的指针偏移，后面 CQE 回来时只要 <code>base + bid * size</code> 就能找回数据。</p>
<p dir="auto">第二，<code>bgid</code> 的管理被封装在 <code>BufferRingGroup</code> 内部。调用方只拿到一个逻辑编号，不直接接触底层注册细节；默认组也在这里顺手建立起来，方便 socket 层提供一个无参的 <code>receive_stream()</code>。</p>
<p dir="auto">第三，归还路径同样应该放在这里，而不是散在各处调用 <code>io_uring_buf_ring_add</code>。因为释放 slot 本质上是 buffer ring 自己的状态变更，和具体是哪条连接、哪次读操作用过这个 slot 没关系。</p>
<p dir="auto">归还逻辑如下：</p>
<pre><code class="language-cpp">void IOContext::BufferRingGroup::release(unsigned bgid, unsigned bid)
{
    auto&amp; buffer_ring = group_[bgid];
    auto* base = static_cast&lt;std::byte*&gt;(buffer_ring.base_address);
    const int offset = buffer_ring.tail &amp; buffer_ring.mask;

    ::io_uring_buf_ring_add(
        buffer_ring.buffer,
        base + bid * buffer_ring.size,
        buffer_ring.size,
        bid,
        buffer_ring.mask,
        offset
    );

    ::io_uring_buf_ring_advance(buffer_ring.buffer, 1);
    ++buffer_ring.tail;
}
</code></pre>
<p dir="auto">一个 slot 被借走时，<code>bid</code> 会随 CQE 一起回来；一个 slot 被归还时，<code>BufferRingGroup</code> 只需要根据同样的 <code>bid</code> 把它重新挂回 ring。这样 buffer 的借出和回收就闭上了环，之后 <code>PooledBuffer</code> 才有可能用 RAII 去包装它。</p>
<hr />
<h3>4. 地基理顺后，再把 multishot 接上</h3>
<p dir="auto"><code>ReceiveStream</code> 真正的提交动作在 <code>arm_operation()</code> 里：</p>
<pre><code class="language-cpp">void arm_operation()
{
    if (!operation_)
        operation_ = new MutishotReceiveOperation{ this, context_, bgid_ };

    auto* sqe = context_-&gt;sqe();
    ::io_uring_prep_recv_multishot(sqe, fd_, nullptr, 0, 0);
    sqe-&gt;flags |= IOSQE_BUFFER_SELECT;
    sqe-&gt;buf_group = bgid_;
    ::io_uring_sqe_set_data(sqe, static_cast&lt;Operation*&gt;(operation_));
    operation_armed_ = true;
}
</code></pre>
<p dir="auto">这个提交动作有两个关键信息。</p>
<p dir="auto">第一，<code>io_uring_prep_recv_multishot</code> 只提交一次，但不是只收一次。只要内核认为这个 multishot 请求还有效，后面每次有数据到达，它都可以继续产出新的 CQE。</p>
<p dir="auto">第二，<code>IOSQE_BUFFER_SELECT</code> 告诉内核：接收缓冲区不由这次 SQE 显式给出，而是从 <code>buf_group</code> 对应的 buffer ring 里挑一个空闲槽位来写。用户态这次不传 <code>void* buf</code>，而是预先把一组固定大小的 buffer 注册给内核，后面由内核按需借用。</p>
<p dir="auto">CQE 回来时，<code>res</code> 是本次收到的字节数，<code>flags</code> 里则可能带上两个额外信息：</p>
<ol>
<li><code>IORING_CQE_F_BUFFER</code>：说明这次结果对应某个 buffer ring 里的槽位。</li>
<li><code>IORING_CQE_F_MORE</code>：说明这个 multishot 请求还活着，后面还会继续收。</li>
</ol>
<p dir="auto">这两个 flag 基本就是 <code>ReceiveStream</code> 最关心的信息：一个告诉它"数据落在哪块 buffer 上"，一个告诉它"这条流还要不要继续等下去"。</p>
<hr />
<h3>5. 最后才是用户侧 API：为什么返回 <code>PooledBuffer</code></h3>
<p dir="auto">如果只是为了把数据交给调用方，返回 <code>std::span&lt;std::byte&gt;</code> 看起来已经够了。但对 <code>ReceiveStream</code> 来说，这远远不够，因为 span 只是一段视图，不携带任何归还语义。</p>
<p dir="auto">内核从 buffer ring 里借出的槽位，最终必须还回去，否则 ring 只会越用越少，直到耗尽。这个归还动作不能指望业务层记得手工调用，所以这里必须做成 RAII。</p>
<p dir="auto"><code>PooledBuffer</code> 的职责正是这个：</p>
<pre><code class="language-cpp">template&lt;typename Context&gt;
class PooledBuffer {
public:
    PooledBuffer(Context&amp; context, unsigned bgid, unsigned bid, std::span&lt;std::byte&gt; buffer)
      : context_{ &amp;context }, bgid_{ bgid }, bid_{ bid }, buffer_{ buffer }
    {}

    ~PooledBuffer()
    {
        release();
    }

    auto data() const noexcept -&gt; std::span&lt;std::byte&gt;
    {
        return buffer_;
    }

private:
    void release()
    {
        if (valid())
            context_-&gt;release_buffer_ring(bgid_, bid_);
    }
};
</code></pre>
<p dir="auto">调用方拿到的是一块"带归还能力的 buffer 句柄"。它可以读这段数据，也可以把这段数据直接交给后续写操作；一旦这个对象离开作用域，对应槽位就自动回到 buffer ring，可供下一次接收复用。</p>
<p dir="auto">这也是 <code>ReceiveStream</code> 最重要的体验差异：上层拿到的是一块已经装好、生命周期清晰、离开作用域后能自动回收的结果，而不是一段裸内存视图。</p>
<hr />
<h3>6. <code>next()</code> 为什么需要 ready queue</h3>
<p dir="auto"><code>ReceiveStream</code> 对外只暴露一个动作：</p>
<pre><code class="language-cpp">auto next() -&gt; NextAwaiter
{
    return NextAwaiter{ *this };
}
</code></pre>
<p dir="auto"><code>next()</code> 能不能用得顺，关键在 <code>NextAwaiter</code> 和 <code>ready_results_</code> 的配合。</p>
<pre><code class="language-cpp">class NextAwaiter {
public:
    auto await_ready() const noexcept -&gt; bool
    {
        return !stream_.ready_results_.empty();
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        stream_.handle_ = handle;

        if (!stream_.operation_armed_)
            stream_.arm_operation();
    }

    auto await_resume() -&gt; std::expected&lt;PooledBuffer&lt;Context&gt;, std::error_code&gt;
    {
        if (stream_.ready_results_.empty())
            return unexpected_system_error(std::errc::operation_canceled);

        auto result = std::move(stream_.ready_results_.front());
        stream_.ready_results_.pop_front();
        return result;
    }
};
</code></pre>
<p dir="auto">这里的 <code>ready_results_</code> 不是装饰品，而是必须存在的一层缓冲。</p>
<p dir="auto"><code>recv_multishot</code> 有一个天然特征：协程还没来得及再次 <code>co_await next()</code>，它就可能已经连续产出了多个 CQE。如果没有队列，后到的结果只能覆盖先到的结果，或者逼着 <code>handle_cqe()</code> 当场恢复协程并同步消化所有数据，这两种都不对。</p>
<p dir="auto">把这段行为画成时序会直观很多：</p>
<pre><code class="language-text">[Kernel multishot]          [ReceiveStream]                 [User Coroutine]
    |                           |                                |
    |-- CQE #1 ----------------&gt;| push ready_results_            |
    |-- CQE #2 ----------------&gt;| push ready_results_            |
    |-- CQE #3 ----------------&gt;| push ready_results_            |
    |                           |                                |
    |                           |&lt;----------- co_await next() ---|
    |                           | pop #1 and resume              |
    |                           |&lt;----------- co_await next() ---|
    |                           | pop #2 and resume              |
</code></pre>
<p dir="auto">也就是说，<code>ready_results_</code> 不是优化项，而是 <code>recv_multishot</code> 能以“生产者-消费者节奏解耦”方式暴露给上层 API 的必要条件。</p>
<p dir="auto">用 <code>std::deque&lt;result_type&gt;</code> 把结果先存起来，问题就顺了：</p>
<ol>
<li>CQE 到达时，先转成 <code>PooledBuffer</code> 或 error，推进队列。</li>
<li>如果此刻真的有协程挂在 <code>next()</code> 上，就恢复它。</li>
<li>如果协程暂时还没来取，结果就老实待在队列里，等下一次 <code>await_ready()</code> 直接命中。</li>
</ol>
<p dir="auto">这样一来，<code>ReceiveStream</code> 就有了一点异步生成器的感觉：底层持续产出，上层按自己的节奏消费，中间靠一个轻量队列解耦。</p>
<hr />
<h3>7. CQE 到达后如何变成可消费结果</h3>
<p dir="auto"><code>handle_cqe()</code> 做的事情，说到底就是把内核语义翻译成库语义。</p>
<pre><code class="language-cpp">void handle_cqe(int result, std::uint32_t flags) noexcept
{
    std::optional&lt;PooledBuffer&lt;Context&gt;&gt; pooled_buffer;

    if (flags &amp; IORING_CQE_F_BUFFER) {
        const auto bid = static_cast&lt;std::uint16_t&gt;(flags &gt;&gt; IORING_CQE_BUFFER_SHIFT);
        auto&amp; buffer_ring = context_-&gt;buffer_ring(bgid_);

        auto* base = static_cast&lt;std::byte*&gt;(buffer_ring.base_address);
        std::span&lt;std::byte&gt; data{ base + bid * buffer_ring.size, static_cast&lt;std::size_t&gt;(result) };
        pooled_buffer.emplace(*context_, bgid_, bid, data);
    }

    if (result == -ECANCELED) {
        ready_results_.emplace_back(unexpected_system_error(std::errc::operation_canceled));
    }
    else if (result &gt;= 0) {
        if (pooled_buffer)
            ready_results_.emplace_back(std::move(*pooled_buffer));
        else
            ready_results_.emplace_back(PooledBuffer&lt;Context&gt;{});
    }
    else {
        ready_results_.emplace_back(unexpected_system_error(-result));
    }

    if (handle_) {
        auto handle = std::exchange(handle_, nullptr);
        handle.resume();
    }
}
</code></pre>
<p dir="auto">这一步分三层：</p>
<p dir="auto">第一层，从 <code>flags</code> 里提取 <code>bid</code>，再结合 <code>bgid_</code> 找回 buffer ring，算出这次数据实际落在了哪一段内存上。</p>
<p dir="auto">第二层，把这段内存包装成 <code>PooledBuffer</code>。从这一刻开始，buffer 的归还责任被显式绑定到了对象生命周期上。</p>
<p dir="auto">第三层，把内核返回码整理成 <code>std::expected</code>：正常数据走 value，取消和错误走 error。这样上层的消费方式就统一了。</p>
<p dir="auto">还有一个细节：<code>result &gt;= 0</code> 但没有 <code>IORING_CQE_F_BUFFER</code> 时，代码会塞一个默认构造的 <code>PooledBuffer</code>。这给上层留出了处理空结果的空间，比如把空 buffer 视为连接关闭。这个约定在 demo 里也直接用到了。</p>
<hr />
<h3>8. 真正难的是收尾</h3>
<p dir="auto"><code>ReceiveStream</code> 最容易写错的，不是提交 multishot，而是收尾。</p>
<p dir="auto">问题在于：当 <code>ReceiveStream</code> 析构时，内核里那笔 multishot 请求不一定已经结束。你当然可以提交一个 cancel SQE，但 cancel 也是异步的，在 cancel 的 CQE 真正回来之前，原来的 multishot CQE 仍然可能再到一次。这个窗口期一旦处理不好，要么 use-after-free，要么 buffer 泄漏。</p>
<p dir="auto">解决办法是：析构时不直接删 operation，而是先 <code>detach()</code>。</p>
<pre><code class="language-cpp">void destroy() noexcept
{
    if (operation_) {
        operation_-&gt;detach();

        auto* sqe = context_-&gt;sqe(false);
        ::io_uring_prep_cancel(sqe, static_cast&lt;Operation*&gt;(operation_), 0);
        ::io_uring_sqe_set_data(sqe, nullptr);

        operation_ = nullptr;
    }

    if (context_)
        context_ = nullptr;
}
</code></pre>
<p dir="auto"><code>detach()</code> 的效果是把 <code>operation</code> 和 <code>ReceiveStream</code> 本体断开。之后如果晚到的 CQE 还落到这笔 operation 上，<code>complete()</code> 会走另一条路径：不再访问已经析构的 stream，而是仅仅把 buffer 槽位补回 buffer ring。</p>
<pre><code class="language-cpp">void complete(int res, unsigned flags) override
{
    const bool has_more = (flags &amp; IORING_CQE_F_MORE) != 0;

    if (stream_) {
        if (!has_more) {
            stream_-&gt;operation_armed_ = false;
            stream_-&gt;operation_ = nullptr;
        }

        stream_-&gt;handle_cqe(res, flags);
    }
    else if (flags &amp; IORING_CQE_F_BUFFER) {
        // stream 已不存在，只负责把槽位补回 buffer ring
        // ...
    }

    if (!has_more)
        delete this;
}
</code></pre>
<p dir="auto">这样一来，<code>ReceiveStream</code> 的析构和 multishot 请求的最终死亡就不再需要严格同步，晚到的 CQE 也不会污染用户态状态，只会做最必要的资源回收。</p>
<p dir="auto">另外，move 构造和 move 赋值里也有一个对应的修正动作：如果 operation 还活着，就把 <code>operation_-&gt;stream_</code> 改指向新的对象。这保证了 <code>ReceiveStream</code> 被移动后，后续 CQE 仍然能回到正确实例上。</p>
<hr />
<h3>9. 实际用法</h3>
<p dir="auto">在 demo 里，Echo Server 的 session 代码已经很干净了：</p>
<pre><code class="language-cpp">auto session(ip::tcp::socket&lt;IOContext&gt; client) -&gt; Task&lt;&gt;
{
    auto stream = client.receive_stream();

    while (true) {
        auto read_result = co_await stream.next();
        if (!read_result) {
            spdlog::warn("Failed to read from client {}: {}",
                         client.native_handle(),
                         read_result.error().message());
            co_return;
        }

        auto received = read_result-&gt;data();
        if (received.empty()) {
            spdlog::info("Client {} disconnected", client.native_handle());
            co_return;
        }

        auto write_result = co_await timeout(async_write(client, received), 1ms);
        if (!write_result)
            co_return;
    }
}
</code></pre>
<p dir="auto">这段代码最直观的变化是：读路径里已经看不到"准备 buffer -&gt; 提交 recv -&gt; 处理 buffer 生命周期"这些样板动作了。业务协程眼里只剩一件事：下一块数据什么时候到。</p>
<p dir="auto">这正是 <code>ReceiveStream</code> 的价值。它不是单纯给 <code>recv_multishot</code> 套了个 awaiter，而是顺手把 buffer ring、生命周期、结果排队和析构期收尾这些麻烦一起包掉了。上层得到的是一个可以连续 <code>next()</code> 的接收流，底层保留的则是 io_uring 多次投递的吞吐优势。</p>
]]></description><link>http://forum.d2learn.org/topic/198/基于-io_uring-的-c-20-协程网络库-09-读路径优化-recv_multishot与receivestream</link><guid isPermaLink="true">http://forum.d2learn.org/topic/198/基于-io_uring-的-c-20-协程网络库-09-读路径优化-recv_multishot与receivestream</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】08 写路径优化：Scatter-Gather与writev]]></title><description><![CDATA[<p dir="auto">在构建了全异步 Echo Server 之后，我们的网络库已经能够处理一来一回的字节流通信。但工程实践中，一个真实的应用往往不会只发送一块连续内存。</p>
<p dir="auto">考虑一个 HTTP/1.1 响应：响应头（<code>std::string</code>）和响应体（<code>std::vector&lt;char&gt;</code>）通常分布在两块不相关的内存区域。最朴素的写法是连续调用两次 <code>async_write_some</code>，但这意味着两次 <code>io_uring</code> 提交、两次协程挂起恢复——这在高并发场景下代价不菲。</p>
<p dir="auto">更糟糕的是，两次独立的写操作并非原子的。在 Nagle 算法被关闭的情况下（<code>TCP_NODELAY</code>），内核可能将头部和体部拆成两个 TCP 报文分别发送，给对端解析器制造不必要的复杂度。</p>
<p dir="auto">本篇将探讨如何通过 <strong>Scatter/Gather I/O</strong> 机制，在单次系统调用中完成对多块内存的原子性聚合写操作。</p>
<hr />
<h3>1. 问题根源：内存布局与系统调用边界</h3>
<p dir="auto">标准的 <code>write(2)</code> 系统调用接受的是一块连续的 <code>(void* buf, size_t count)</code>。要发送分散在多处的数据，传统上有两种方案：</p>
<ol>
<li><strong>拼接再发送</strong>：将所有数据 <code>memcpy</code> 到一块连续缓冲区，再调用一次 <code>write</code>。代价是额外的内存分配与拷贝，延迟增加，CPU 缓存命中率下降。</li>
<li><strong>多次调用</strong>：依次对每块数据各调用一次 <code>write</code>。代价是多次用户态 <img src="http://forum.d2learn.org/assets/plugins/nodebb-plugin-emoji/emoji/android/2194.png?v=c6uec18e3vu" class="not-responsive emoji emoji-android emoji--left_right_arrow" style="height:23px;width:auto;vertical-align:middle" title=":left_right_arrow:" alt="↔" /> 内核态的上下文切换，以及不可避免的时序问题。</li>
</ol>
<p dir="auto">POSIX 标准的答案是 <code>writev(2)</code>：</p>
<pre><code class="language-c">ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
</code></pre>
<p dir="auto">调用方构造一个 <code>iovec</code> 数组，每个元素描述一块内存区域，内核在内部完成聚合，对外表现为<strong>单次、原子的写操作</strong>。这种模式被称为 <strong>Gather Write</strong>（聚合写），与之对应的 <code>readv</code> 称为 <strong>Scatter Read</strong>（分散读），统称 <strong>Scatter/Gather I/O</strong>。</p>
<pre><code class="language-c">struct iovec {
    void  *iov_base;  // 缓冲区起始地址
    size_t iov_len;   // 缓冲区长度
};
</code></pre>
<hr />
<h3>2. Concept 先行：定义 Buffer 序列的语言契约</h3>
<p dir="auto">C++ 标准库没有"一组只读 buffer 的 range"这个抽象，我们用两个 Concept 来定义它：</p>
<pre><code class="language-cpp">template&lt;typename T&gt;
concept const_buffer = requires(const T&amp; t)
{
    { buffer(t) } -&gt; std::same_as&lt;std::span&lt;const std::byte&gt;&gt;;
};

template &lt;typename T&gt;
concept sequence_buffer = std::ranges::range&lt;T&gt; 
                       &amp;&amp; const_buffer&lt;std::ranges::range_reference_t&lt;T&gt;&gt;;
</code></pre>
<p dir="auto"><code>std::string</code>、<code>std::vector&lt;char&gt;</code>、<code>std::string_view</code> 等连续存储类型本身就满足 <code>const_buffer</code>。因此 <code>std::vector&lt;std::string&gt;</code> 或 <code>std::array&lt;std::string_view, N&gt;</code> 这类 range 可以直接传给 <code>async_write_some</code>，不需要用户手动做任何转换。</p>
<hr />
<h3>3. 异步实现：<code>WriteSequenceAwaiter</code> 的内存安全设计</h3>
<p dir="auto">异步化 <code>writev</code> 有一个绕不开的内存安全问题：<code>iov</code> 数组的生命周期。</p>
<pre><code class="language-cpp">template&lt;sequence_buffer Buffer&gt;
class WriteSequenceAwaiter: public Operation {
public:
    WriteSequenceAwaiter(context_type&amp; context, int socket, const Buffer&amp; buffers)
      : context_(context), socket_(socket)
    {
        iov_.reserve(std::ranges::size(buffers));

        for (const auto&amp; chunk : buffers) {
            auto data = buffer(chunk);
            iov_.push_back(::iovec{
                .iov_base = const_cast&lt;std::byte*&gt;(data.data()),
                .iov_len  = data.size()
            });
        }
    }

    void prepare(::io_uring_sqe* sqe) noexcept override
    {
        ::io_uring_prep_writev(sqe, socket_, iov_.data(), iov_.size(), 0);
    }

    // ...
private:
    std::vector&lt;::iovec&gt; iov_;
    // ...
};
</code></pre>
<p dir="auto">SQE 提交后，内核在某个未知的时间点才真正执行 I/O，等待期间 <code>iov</code> 数组必须保持有效。<code>WriteSequenceAwaiter</code> 的做法是<strong>在构造函数中把 <code>iovec</code> 元数据全部拷贝进 <code>iov_</code> 成员向量</strong>。</p>
<p dir="auto">这里有一个关键的区分：</p>
<ul>
<li><strong><code>iovec</code> 元数据（指针 + 长度）</strong>：占用空间极小（每个 16 字节），在构造时拷贝，由 Awaiter 对象自身持有。</li>
<li><strong>实际的 payload 字节</strong>：不拷贝，<code>iov_base</code> 直接指向调用方提供的原始内存。</li>
</ul>
<p dir="auto">由于 Awaiter 对象驻留在协程帧中，而协程帧的生命周期与 <code>co_await</code> 表达式完全绑定——<code>co_await</code> 在整个异步操作期间协程不会销毁帧，因此：</p>
<ol>
<li><code>iov_</code> 向量本身由协程帧持有，直到 <code>complete()</code> 恢复后才释放——内核读取 <code>iov</code> 数组时内存有效。</li>
<li>payload 字节由调用方保证在 <code>co_await</code> 期间有效，<code>iov_base</code> 指针始终合法。</li>
</ol>
<p dir="auto"><code>iov_</code> 元数据由 Awaiter 自己持有，payload 字节一字节不动——零拷贝，正确性靠协程帧的生命周期保证。</p>
<hr />
<h3>4. 暴露给用户的 API：<code>async_write_some</code> 的重载决议</h3>
<p dir="auto"><code>WriteSequenceAwaiter</code> 最终通过 <code>StreamSocket</code> 上的重载函数暴露给用户：</p>
<pre><code class="language-cpp">// 重载一：单块 buffer
auto async_write_some(std::span&lt;const std::byte&gt; buffer) noexcept 
    -&gt; async::WriteSomeAwaiter;

// 重载二：buffer 序列
template&lt;sequence_buffer Buffer&gt;
auto async_write_some(const Buffer&amp; buffer) noexcept 
    -&gt; async::WriteSequenceAwaiter&lt;Buffer&gt;;
</code></pre>
<p dir="auto">两个重载共享同一个函数名，编译器依据参数类型自动选择正确的路径：</p>
<ul>
<li>传入 <code>std::span&lt;const std::byte&gt;</code>（或满足隐式转换的单一连续 range）→ 走 <code>WriteSomeAwaiter</code>，底层是 <code>io_uring_prep_write</code></li>
<li>传入满足 <code>sequence_buffer</code> 的 range（如 <code>std::vector&lt;std::string_view&gt;</code>）→ 走 <code>WriteSequenceAwaiter</code>，底层是 <code>io_uring_prep_writev</code></li>
</ul>
<hr />
<h3>5. 实战：发送分散的 HTTP 响应</h3>
<p dir="auto">以发送一个 HTTP/1.1 响应为例，头部和体部分别存储在两块独立内存中：</p>
<pre><code class="language-cpp">auto send_response(net::ip::tcp::socket&amp; conn) -&gt; async::Task&lt;&gt;
{
    std::string header = 
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/plain\r\n"
        "Content-Length: 13\r\n"
        "\r\n";

    std::string body = "Hello, World!";

    // std::string 直接满足 const_buffer，std::array&lt;std::string, 2&gt; 满足 sequence_buffer
    std::array parts = { header, body };

    // 一次 co_await → 一次 io_uring 提交 → 一次内核 writev
    auto result = co_await conn.async_write_some(parts);
    if (!result) {
        spdlog::warn("send failed: {}", result.error().message());
        co_return;
    }

    spdlog::info("sent {} bytes", *result);
}
</code></pre>
<p dir="auto">与朴素的两次 <code>async_write_some</code> 调用相比，这里只有<strong>一次协程挂起恢复</strong>，且头部与体部保证在同一次 <code>writev</code> 中原子发送——在 <code>TCP_NODELAY</code> 的场景下尤为重要。</p>
<hr />
<h3>6. "写完为止"与"读完为止"：WriteAll 与 ReadAll</h3>
<p dir="auto"><code>async_write_some</code> 和 <code>async_read_some</code> 只保证<strong>单次内核调用</strong>——它们返回的是"这次实际传输了多少字节"，而非"请求的字节是否全部完成"。在流式协议下，这种部分传输（partial I/O）是完全合法的内核行为。</p>
<p dir="auto">"写完为止"在网络编程中足够常见，值得在库层面直接封装。<code>WriteAllAwaiter</code> 和 <code>ReadAllAwaiter</code> 将"重试直到完成"的逻辑封装在 Awaiter 内部，业务层无需感知部分传输的存在：</p>
<h4>6.1 WriteAllAwaiter</h4>
<pre><code class="language-cpp">class WriteAllAwaiter: public CancelableOperation {
    // ...
    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        if (error_code_ != 0 || buffer_.empty())
            resume(handle_, result, flags);   // 完成或出错，恢复协程
        else
            arm_write();                       // 还有剩余，重新提交
    }

    void arm_write() noexcept
    {
        auto* sqe = context_.sqe();
        ::io_uring_prep_send(sqe, socket_, buffer_.data(), buffer_.size(), 0);
        ::io_uring_sqe_set_data(sqe, this);
    }

    void set_result(int result, std::uint32_t flags) noexcept
    {
        if (result &gt; 0) {
            bytes_written_ += static_cast&lt;std::size_t&gt;(result);
            buffer_ = buffer_.subspan(result);   // 滑动视图，指向剩余部分
        }
        else if (result == 0) {
            error_code_ = ECONNABORTED;          // 对端关闭连接
        }
        else {
            error_code_ = -result;
        }
    }
};
</code></pre>
<p dir="auto"><code>buffer_</code> 是 <code>std::span&lt;const std::byte&gt;</code>，只是视图，不持有数据。每次部分写入后，<code>set_result</code> 把视图头部前移；<code>complete()</code> 随即检查：还有剩余就调 <code>arm_write()</code> 重新提交 SQE，写完了或出错了才调 <code>resume()</code> 恢复协程。</p>
<p dir="auto">这里有个值得留意的细节：重试不能用循环实现。<code>complete()</code> 是内核 CQE 的回调路径，不能在里面等待下一次 I/O 完成——唯一的方式是重新提交一个 SQE，让内核完成后再次回调。这也是 <code>arm_write()</code> 会在 <code>complete()</code> 里再次出现的原因。协程从头到尾只挂起一次，中间所有的重试都在回调链里发生，外面看不到任何细节。</p>
<p dir="auto"><code>ReadAllAwaiter</code> 与之对称，换成 <code>io_uring_prep_recv</code> 和可写的 <code>std::span&lt;std::byte&gt;</code>。<code>result == 0</code> 时返回 <code>ECONNABORTED</code>——对端已关闭，已读字节不足期望长度，继续等下去不会再有新数据。</p>
<h4>6.2 取消机制：为什么不能用 link_timeout？</h4>
<p dir="auto">在第三篇博客中，我们为一次性的 I/O 操作（<code>async_read_some</code>、<code>async_write_some</code>）实现了超时机制，其底层是 io_uring 的 <strong><code>IOSQE_IO_LINK</code> + <code>io_uring_prep_link_timeout</code></strong> 方案：将一个 timeout SQE 通过 <code>IOSQE_IO_LINK</code> 链接到 I/O SQE 后面，内核自动保证"哪个先完成，另一个被取消"。</p>
<p dir="auto">但这一方案对 <code>WriteAllAwaiter</code> / <code>ReadAllAwaiter</code> 完全失效，原因在于它们是<strong>多次提交</strong>的操作——每次部分写入后，<code>complete()</code> 回调里会再次调用 <code>arm_write()</code> 提交新的 SQE。而 <code>IOSQE_IO_LINK</code> 只能链接<strong>相邻的两个</strong> SQE，第一次提交时的链接在第一个 CQE 到达后就已经消耗完毕，无法覆盖后续的重试 SQE。</p>
<p dir="auto">为此，这里引入了一套针对多次提交操作的取消机制，核心是 <code>CancelableOperation</code> 里的一个 <code>parent</code> 指针：</p>
<pre><code class="language-cpp">struct CancelableOperation : public Operation {
    CancelableOperation* parent{ nullptr };

    void resume(std::coroutine_handle&lt;&gt; handle, int result, std::uint32_t flags) noexcept
    {
        if (parent)
            parent-&gt;complete(result, flags);   // 路由给包装层
        else if (handle)
            std::exchange(handle, nullptr).resume();
    }
};
</code></pre>
<p dir="auto"><code>CancelableOperation</code> 在 <code>Operation</code> 基础上新增了一个 <code>parent</code> 指针。当操作被一个超时组合器包裹时，<code>parent</code> 被设置为该组合器；否则为 <code>nullptr</code>，行为与普通 <code>Operation</code> 相同。</p>
<p dir="auto"><code>WriteAllAwaiter</code> 继承自 <code>CancelableOperation</code>，并在 <code>complete()</code> 中通过 <code>resume()</code> 而非直接调用 <code>handle_.resume()</code>。关键在于 <code>resume()</code> 只在<strong>整个操作最终完成或出错</strong>时才被调用——中间每次部分写入完成后，<code>complete()</code> 直接调用 <code>arm_write()</code> 重新提交，不经过 parent。只有当 <code>buffer_.empty()</code>（写完）或 <code>error_code_ != 0</code>（出错/被取消）时，才通过 <code>resume()</code> 将结果路由出去。这样 parent 只需处理一次最终事件，而不是每次重试。</p>
<p dir="auto">这段逻辑如果只看文字会比较绕，可以把它压成一个事件时序：</p>
<pre><code class="language-text">[User Coroutine]          [TimeoutCombinator]             [io_uring]
    |                         |                            |
    |-- co_await timeout() --&gt;|                            |
    |                         |-- submit timer SQE -------&gt;|
    |                         |-- submit write_all SQE ---&gt;|
    |                         |                            |
    |                         |&lt;-- CQE(write partial) ---- |  (继续 arm_write)
    |                         |&lt;-- CQE(write done) ---- ---|  (或 error)
    |                         |-- cancel(timer) ----------&gt;|
    |                         |&lt;-- CQE(timer canceled) ----|
    |&lt;------------------------|  resume once               |
</code></pre>
<p dir="auto">另一条分支是 timer 先到期：<code>TimeoutCombinator</code> 会反向 cancel 当前飞行中的 write/read SQE，同样等两边 CQE 都收干净后再恢复协程。核心目标只有一个：<strong>恢复一次，但把飞行中的请求收尾做完整</strong>。</p>
<p dir="auto">有了这个基础，<code>TimeoutCombinator</code> 就可以实现真正的独立定时器了：</p>
<pre><code class="language-cpp">template&lt;cancelable_operation Awaiter&gt;
class TimeoutCombinator: public CancelableOperation {
    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;

        // 独立提交一个定时器 SQE
        auto* sqe = context().sqe();
        ::io_uring_prep_timeout(sqe, &amp;timeout_, 0, 0);
        ::io_uring_sqe_set_data(sqe, &amp;timer_);

        // 再提交内层操作（内层操作的 parent 已在构造时设为 this）
        awaiter_.await_suspend(handle);
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        // 内层操作（某次重试）先完成了——取消定时器
        if (state_ == State::Pending) {
            state_ = State::AwaiterCompleted;
            auto* sqe = context().sqe(false);
            ::io_uring_prep_cancel(sqe, &amp;timer_, 0);
        }
        if (--pending_cqes_ == 0)
            std::exchange(handle_, {}).resume();
    }

    void on_timer_completed(int result) noexcept
    {
        // 定时器先到——取消内层操作当前正在飞行的 SQE
        if (state_ == State::Pending) {
            state_ = State::TimerCompleted;
            auto* sqe = context().sqe(false);
            ::io_uring_prep_cancel(sqe, &amp;awaiter_, 0);
        }
        if (--pending_cqes_ == 0)
            std::exchange(handle_, {}).resume();
    }
};
</code></pre>
<p dir="auto">与 <code>TimeoutAwaiter</code> 的 <code>IOSQE_IO_LINK</code> 不同，<code>TimeoutCombinator</code> 将定时器 SQE 和内层操作 SQE <strong>独立提交</strong>，两者在 io_uring 中是平等的并发请求：</p>
<ul>
<li><strong>内层操作先完成</strong>：通过 <code>parent-&gt;complete()</code> 进入 <code>TimeoutCombinator::complete()</code>，发出 <code>io_uring_prep_cancel</code> 取消定时器，等待定时器的 CQE 到达后恢复协程。</li>
<li><strong>定时器先到期</strong>：<code>Timer::complete()</code> 调用 <code>on_timer_completed()</code>，发出 <code>io_uring_prep_cancel</code> 取消当前正在飞行的 <code>awaiter_</code> SQE，等待其 CQE 到达后以 <code>timed_out</code> 恢复协程。</li>
</ul>
<p dir="auto">两种情况都要求 <strong><code>pending_cqes_</code>（初值为 2）减到零</strong>才恢复协程——这保证了无论竞争结果如何，所有飞行中的 SQE 的 CQE 最终都被消耗掉，不会残留在完成队列中干扰后续操作。</p>
<p dir="auto">从调用方视角来看，两套机制的接口完全相同：</p>
<pre><code class="language-cpp">// async_read_some：单次提交 → 走 TimeoutAwaiter（link_timeout）
auto r1 = co_await timeout(socket.async_read_some(buf), 5s);

// async_read_all：多次提交 → 走 TimeoutCombinator（独立定时器 + cancel）
auto r2 = co_await timeout(socket.async_read_all(buf), 5s);
</code></pre>
<p dir="auto"><code>timeout()</code> 函数通过两个重载，依据 <code>single_shot_only_operation</code> 和 <code>cancelable_operation</code> 两个 Concept 在编译期自动分发，调用方无需关心底层选择了哪套机制。</p>
<hr />
<h4>6.3 为什么不需要序列版 WriteAll</h4>
<p dir="auto">看到这里，你可能会问：我们有 <code>WriteSequenceAwaiter</code>（序列版 <code>write_some</code>），是否也需要一个序列版 <code>write_all</code>？</p>
<p dir="auto">答案是<strong>不需要</strong>。</p>
<p dir="auto"><code>writev</code> 的关键语义是<strong>原子性</strong>：内核保证整个 <code>iovec</code> 数组作为一个整体提交给协议栈。对于流式套接字（<code>SOCK_STREAM</code>），内核要么接受全部数据进入发送缓冲区，要么在缓冲区不足时只接受一部分。</p>
<p dir="auto">但这里有一个根本性的约束：<strong><code>writev</code> 的部分写入发生后，你无法简单地"重试剩余部分"</strong>。每次 <code>writev</code> 写入 N 字节后，你需要遍历 <code>iovec</code> 数组，跳过已完整写入的 chunk，并修剪部分写入那个 chunk 的 <code>iov_base</code>/<code>iov_len</code>，然后以剩余的 <code>iovec</code> 子集重新提交：</p>
<pre><code class="language-cpp">// 如果要实现序列版 write_all，必须处理这种修剪逻辑
void advance(std::size_t n) {
    while (n &gt; 0 &amp;&amp; !iov_.empty()) {
        if (n &gt;= iov_.front().iov_len) {
            n -= iov_.front().iov_len;
            iov_.erase(iov_.begin());          // O(n) erase，或改用 index
        } else {
            auto* base = static_cast&lt;char*&gt;(iov_.front().iov_base);
            iov_.front().iov_base = base + n;
            iov_.front().iov_len -= n;
            n = 0;
        }
    }
}
</code></pre>
<p dir="auto">这并非不可实现，但代价是显著的复杂度提升，而实际收益却微乎其微——实践中发送缓冲区充足时 <code>writev</code> 极少发生部分写入。</p>
<p dir="auto">更重要的是，<code>writev</code> 的使用场景本身就决定了调用方通常不关心"是否全部写完"：当你用 <code>writev</code> 拼装一个 HTTP 响应时，你的目标是<strong>原子地将头部和体部交给内核</strong>，至于内核何时真正通过 TCP 发出去，不是这一层要操心的。这与 <code>write_all</code> 的语义（"确保用户层的所有字节都离开应用缓冲区"）本质上是两个不同的问题。</p>
<p dir="auto">因此，我们的设计决策是：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>场景</th>
<th>API 选择</th>
</tr>
</thead>
<tbody>
<tr>
<td>一次性发送多块分散内存</td>
<td><code>async_write_some(sequence_buffer)</code> → <code>writev</code></td>
</tr>
<tr>
<td>确保单块内存完整写入</td>
<td><code>async_write_all(span)</code> → 循环 <code>send</code></td>
</tr>
<tr>
<td>既要分散又要保证全部写完</td>
<td>希望没有这种需求</td>
</tr>
</tbody>
</table>
]]></description><link>http://forum.d2learn.org/topic/197/基于-io_uring-的-c-20-协程网络库-08-写路径优化-scatter-gather与writev</link><guid isPermaLink="true">http://forum.d2learn.org/topic/197/基于-io_uring-的-c-20-协程网络库-08-写路径优化-scatter-gather与writev</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】07 实现Acceptor]]></title><description><![CDATA[<p dir="auto">在上一篇文章中，我们构建了 <code>StreamSocket</code>，它作为面向连接的流式套接字，完美解决了客户端的主动连接（<code>connect</code>）与边界安全的字节流传输问题。</p>
<p dir="auto">然而，网络通信是双向的。对于服务端而言，我们需要一种截然不同的实体：它不负责读写数据，只负责被动监听并生产新的连接。在 POSIX 网络栈中，这就是监听套接字（Listening Socket）。</p>
<p dir="auto">本篇文章将探讨服务端组件 <code>BasicAcceptor</code> 的设计，并在文章的最后，利用我们迄今为止构建的所有基础设施，跑通一个完整的、零运行时开销的全异步 Echo Server。</p>
<h3>1. 架构修正：为什么将 <code>bind</code> 下沉至 <code>BaseSocket</code>？</h3>
<p dir="auto">在着手编写 <code>Acceptor</code> 之前，我们必须先纠正一个在上一章中犯的经验主义错误。</p>
<p dir="auto">在许多入门教程中，<code>bind</code> 似乎永远是服务端的专利（配合 <code>listen</code> 使用），而客户端只需要 <code>connect</code>。如果按照这个逻辑，<code>bind</code> 应该被封装在 <code>Acceptor</code> 的初始化中。</p>
<p dir="auto">但这在真实的工业级场景中是行不通的，考虑以下场景：</p>
<ul>
<li><strong>多网卡流量隔离：</strong> 客户端在 <code>connect</code> 之前，常常需要显式 <code>bind</code> 到特定的源 IP（例如万兆专线网卡）以控制出口路由。</li>
<li><strong>NAT 穿透与端口复用：</strong> 客户端可能需要绑定固定端口来配合 P2P 打洞。</li>
</ul>
<p dir="auto">从操作系统的视角看，<strong>任何初始状态的文件描述符都具备被 <code>bind</code> 的物理能力</strong>。因此，在开发 <code>Acceptor</code> 之前，我们将 <code>bind</code> 能力从特定组件中剥离，<strong>下沉到了最底层的 <code>BaseSocket</code> 中</strong>。</p>
<h3>2. 核心定位：强类型约束的“连接工厂”</h3>
<p dir="auto">明确了基础设施后，我们来看 <code>BasicAcceptor</code> 的定义。它继承自 <code>BaseSocket</code>，剥离了一切读写接口，其唯一的职责是作为连接的生产工厂。</p>
<pre><code class="language-cpp">template&lt;typename Protocol, typename Context&gt;
class BasicAcceptor: public BaseSocket&lt;Protocol, Context&gt; {
public:
    // 强制静态推导：产出物必须与协议完全匹配
    using socket_type = typename Protocol::template socket&lt;Context&gt;;
    using endpoint_type = typename Protocol::endpoint;
    using base_type = BaseSocket&lt;Protocol, Context&gt;;
    // ...
};
</code></pre>
<p dir="auto">这里的Type Traits至关重要。<code>socket_type</code> 确保了 <code>ip::tcp::acceptor</code> 产出的永远是被强类型保护的 <code>ip::tcp::socket</code>（即 <code>StreamSocket</code>），从编译器层面切断了将 TCP 连接误用为 UDP 套接字的可能。</p>
<h3>3. 固化初始化序列与 SO_REUSEADDR</h3>
<p dir="auto">服务端的启动逻辑必须遵循严格的物理规约：配置选项 -&gt; <code>bind</code> -&gt; <code>listen</code>。其中最隐蔽的陷阱是 <code>SO_REUSEADDR</code> 选项的时序问题。</p>
<p dir="auto">当服务端进程崩溃或更新重启时，旧的 TCP 连接可能仍处于 <code>TIME_WAIT</code> 状态。此时如果直接 <code>bind</code>，内核会抛出 <code>EADDRINUSE</code> (Address already in use) 错误。为了提升系统的重启弹性，<strong><code>SO_REUSEADDR</code> 必须在 <code>bind</code> 之前设置。</strong></p>
<p dir="auto">我们在 <code>BasicAcceptor</code> 的构造函数中，强制实行了这套安全的初始化流程：</p>
<pre><code class="language-cpp">using reuse_address = BooleanOption&lt;SOL_SOCKET, SO_REUSEADDR&gt;;
using reuse_port = BooleanOption&lt;SOL_SOCKET, SO_REUSEPORT&gt;;

BasicAcceptor(Context&amp; context, const endpoint_type&amp; endpoint, bool enable_reuse_port = false)
    : base_type{ context, endpoint.protocol() }
{
    this-&gt;option(reuse_address{ true });

    if (enable_reuse_port)
        this-&gt;option(reuse_port{ true });

    this-&gt;bind(endpoint);
    listen(MAX_LISTEN_CONNECTIONS);
}
</code></pre>
<p dir="auto">对于需要横向扩展的多 Worker 进程架构，我们还提供了一个额外的配置项，允许显式开启 <code>SO_REUSEPORT</code>，将底层的连接负载均衡交由 Linux 内核的 TCP/IP 协议栈处理。</p>
<p dir="auto">除此之外，用户也可以通过延迟打开的方式，自己配置相对应的option</p>
<pre><code class="language-c++">auto acceptor = ip::tcp::accptor{ io_contex };
// 手动打开socket
acceptor.open(ep.protocol());

// 配置需要的option
acceptor.option(reuse_address{ true });

// 注意bind的先后顺序
acceptor.bind(ep);
acceptor.listen();
</code></pre>
<h3>4. 异步抽象：获取对端元数据</h3>
<p dir="auto">在很多业务场景（如访问控制、日志审计）中，服务端需要知道新连接的源 IP 和端口。此时，我们需要提供一个接收 <code>endpoint_type</code> 引用的 <code>async_accept</code> 重载版本。</p>
<p dir="auto">为了支持这一特性，我们需要稍微扩展底层的 <code>AcceptAwaiter</code>。内核的 <code>accept</code> 系统调用要求传入一个 <code>sockaddr*</code> 指针和一个保存结构体长度的 <code>socklen_t*</code> 指针作为输入输出（In/Out）参数。</p>
<pre><code class="language-cpp">template&lt;typename Protocol, typename Context&gt;
class AcceptAwaiter: public Operation {
public:
    using socket_type = typename Protocol::template socket&lt;Context&gt;;
    using endpoint_type = typename Protocol::endpoint;
    // ...

    AcceptAwaiter(Context&amp; context, int fd, endpoint_type* peer = nullptr)
      : context_{ context }, fd_{ fd }, peer_{ peer }
    {
        if (peer_)
            addrlen_ = peer_-&gt;capacity(); // 获取底层缓冲区的最大安全容量
    } 

    void prepare(::io_uring_sqe* sqe) noexcept override
    {
        // addrlen_ 将由内核覆写为实际的地址长度
        ::io_uring_prep_accept(sqe, 
                               fd_, 
                               peer_ ? peer_-&gt;data() : nullptr, 
                               peer_ ? &amp;addrlen_ : nullptr, 
                               0);
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        // 不要忘了在返回前resize endpoint，虽然在ip::BasicEndpoint中我们使用的定长类型，但别的协议就需要resize了。
        // 查看ip::BasicEndpoint的resize实现的话，你会发现那是一个空函数
        if (peer_ &amp;&amp; result &gt;= 0)
            peer_-&gt;resize(addrlen_);

        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

    // ...
private:
    Context&amp; context_;
    int fd_;
    endpoint_type* peer_;
    socklen_t addrlen_; 

    // ...
};
</code></pre>
<p dir="auto">借由 <code>Endpoint</code> 内部基于 <code>sockaddr_storage</code> 构建的内存缓冲区，无论对端是 IPv4 还是 IPv6，我们都能安全地承载内核写入的地址信息。</p>
<p dir="auto">至此，<code>Acceptor</code> 可以在协程流中优雅地捕获对端信息：</p>
<pre><code class="language-cpp">auto async_accept(endpoint_type&amp; endpoint) noexcept -&gt; AcceptAwaiter&lt;Protocol, Context&gt;
{
    return AcceptAwaiter&lt;Protocol, Context&gt;{ context(), native_handle(), &amp;endpoint };
}
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/src/acceptor.h" rel="nofollow ugc">完整代码</a></p>
<h3>5. 全异步 Echo Server 实战</h3>
<p dir="auto">基础设施拼图现已全部齐备（<code>IOContext</code>、<code>Endpoint</code>、<code>StreamSocket</code>、<code>Acceptor</code>、协程调度机制）。是时候用几行极其简练的 C++20 代码，检验这套系统的真实战力了。</p>
<p dir="auto">我们将编写一个全异步的 TCP Echo Server。它包含两个核心的协程流：<strong>会话流（Session）<strong>与</strong>分发流（Echo）</strong>。</p>
<h4>5.1 业务逻辑：Session 协程</h4>
<p dir="auto"><code>session</code> 协程负责处理单一的客户端连接。在协程的作用下，原本复杂的异步状态机被拉平为直观的 <code>while(true)</code> 同步循环流。我们使用 <code>std::expected</code> 的返回值配合 <code>co_await</code>，在局部范围内完成了非阻塞的 I/O 与异常处理。</p>
<pre><code class="language-cpp">#include &lt;cstdlib&gt;
#include &lt;iostream&gt;
#include &lt;spdlog/spdlog.h&gt;
#include "co_spawn.h"
#include "io_context.h"
#include "ip/tcp.h"
#include "task.h"
#include "timeout.h"
#include "buffer.h"

auto session(ip::tcp::socket&lt;IOContext&gt; client) -&gt; Task&lt;&gt;
{
    auto data = std::string(1024, '\0');

    while (true) {
        // 1. 异步读取，协程挂起，零线程阻塞
        // 如果需要的话，你也可以套一个timeout。不过不要忘了除了timedout错误
        auto read_result = co_await client.async_read_some(buffer(data));
        if (!read_result) {
            spdlog::warn("Failed to read from client {}: {}", client.native_handle(), read_result.error().message());
            co_return;
        }

        auto bytes_read = *read_result;
        if (bytes_read == 0) {
            spdlog::info("Client {} disconnected", client.native_handle());
            co_return;
        }

        spdlog::info("Data from client {}: {}", client.native_handle(), data);

        auto write_buffer = buffer(data.substr(0, bytes_read));

        // 2. 利用 std::span 提取有效数据视图，异步写回
        using namespace std::literals::chrono_literals;
        auto write_result = co_await timeout(client.async_write_some(write_buffer), 5s);
        if (!write_result) {
            if (write_result.error() == std::errc::timed_out)
                spdlog::warn("Write to client {} timed out", client.native_handle());
            else
                spdlog::warn("Failed to write to client {}: {}", client.native_handle(), write_result.error().message());

            co_return;
        }
    }
}
</code></pre>
<h4>5.2 监听派发：Echo 协程与主循环</h4>
<p dir="auto"><code>echo</code> 协程使用 <code>Acceptor</code> 监听本地端口。当内核将新连接投递到 <code>io_uring</code> 的完成队列（CQE）时，<code>echo</code> 协程被唤醒，并通过 <code>co_spawn</code> 将接收到的强类型 Socket 所有权移交给新的 <code>session</code> 协程。</p>
<pre><code class="language-cpp">auto echo(IOContext&amp; context) -&gt; Task&lt;&gt;
{
    // auto endpoint = ip::tcp::endpoint::from_string("127.0.0.1", 12345);
    auto endpoint = ip::tcp::endpoint{ ip::AddressV6::loopback(), 12345 };
    std::cout &lt;&lt; "Server listening on " &lt;&lt; endpoint &lt;&lt; "\n";

    auto acceptor = ip::tcp::acceptor{ context, endpoint };

    auto client_endpoint = ip::tcp::endpoint{};
    while (true) {
        auto client = co_await acceptor.async_accept(client_endpoint);
        if (!client) {
            spdlog::warn("Failed to accept client connection: {}", client.error().message());
            continue;
        }

        spdlog::info("Accepted connection from {}:{}", client_endpoint.address().to_string(), client_endpoint.port());
        co_spawn(context, session(std::move(*client)));
    }
}

int main(int argc, char* argv[])
{
    IOContext context;

    // 启动监听协程
    co_spawn(context, echo(context));

    // 启动 io_uring 事件循环
    context.run();

    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/tcp_ip.cpp" rel="nofollow ugc">完整代码</a></p>
<p dir="auto">运行结果</p>
<pre><code class="language-bash">[2026-04-23 15:49:48.030] [info] Accepted connection from ::1:37696
[2026-04-23 15:49:49.471] [warning] Data from client 7: dsaf

[2026-04-23 15:49:51.885] [warning] Data from client 7: dasaasdd

[2026-04-23 15:49:54.124] [info] Accepted connection from ::1:48684
[2026-04-23 15:49:55.462] [warning] Data from client 8: fasasdasdfas

[2026-04-23 15:49:55.987] [info] Client 8 disconnected
[2026-04-23 15:49:57.515] [info] Client 7 disconnected
^C[2026-04-23 15:49:59.360] [info] Received shutdown signal, stopping IOContext...
</code></pre>
<h3>结语</h3>
<p dir="auto">我们利用 C++20 的协程机制彻底抹平了异步 I/O 的认知鸿沟；利用 <code>io_uring</code> 将系统调用的开销降至极限；更重要的是，利用现代 C++ 的类型萃取与所有权语义（RAII &amp; Move Semantics），我们将资源泄漏、类型混用等致命的系统级并发问题，统统拦截在了编译期。</p>
<p dir="auto">别的类型的socket我们不做介绍，和stream socket大体上都是相同的。后续会回归主线，介绍writev以及uring buffer环形队列的协程接口封装。</p>
]]></description><link>http://forum.d2learn.org/topic/195/基于-io_uring-的-c-20-协程网络库-07-实现acceptor</link><guid isPermaLink="true">http://forum.d2learn.org/topic/195/基于-io_uring-的-c-20-协程网络库-07-实现acceptor</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】06 socket层次化封装]]></title><description><![CDATA[<p dir="auto">在构建了底层的异步轮询引擎（<code>IOContext</code>）、协程机制以及端点（<code>Endpoint</code>）的内存布局后，我们进入网络编程的核心实体：套接字（Socket）。</p>
<p dir="auto">在早期的概念验证阶段（第四篇博客中），为了快速验证系统可行性，我们曾实现过一个简陋的 <code>Socket</code> 类，将描述符的创建、<code>bind</code>、<code>listen</code>、<code>accept</code>、<code>read</code> 和 <code>write</code> 糅合在一个结构中。</p>
<p dir="auto">作为原型，它是合格的。但作为工业级的基础设施，这种设计存在致命的类型安全隐患：如果一个用于 UDP 的数据报套接字暴露了 <code>listen()</code> 和 <code>accept()</code> 接口，编译器并不会报错，逻辑谬误只会在运行时以 <code>-EOPNOTSUPP</code>（Operation not supported）的形式暴露。</p>
<p dir="auto">本篇的目标是从宏观架构出发，构建一个职责单一、零运行时开销，且受 C++20 强类型系统严格保护的套接字层级体系。</p>
<hr />
<h3>1. 宏观架构</h3>
<p dir="auto">POSIX 系统为网络通信提供了极度灵活但也极其松散的 C API。在现代 C++ 中，核心接口设计原则是：<strong>让接口易于正确使用，难以被误用</strong>。为此，我们必须根据网络协议的物理行为特征，将 Socket 拆解为层次分明的类簇：</p>
<ol>
<li>
<p dir="auto"><strong><code>BaseSocket</code></strong>：<br />
所有套接字的基类。其唯一职责是：<strong>基于 RAII 原则管理文件描述符（fd）的生命周期</strong>，并提供底层统一的套接字选项（Socket Options）配置接口。</p>
</li>
<li>
<p dir="auto"><strong><code>Acceptor</code>（被动接收器）</strong>：<br />
继承自 <code>BaseSocket</code>。专门用于服务端监听。仅开放 <code>bind</code>、<code>listen</code> 和 <code>accept</code> 接口。它剥离了数据读写能力，因为监听套接字本身不应参与数据载荷的收发。</p>
</li>
<li>
<p dir="auto"><strong><code>StreamSocket</code>（流式套接字）</strong>：<br />
继承自 <code>BaseSocket</code>。代表<strong>面向连接、可靠的字节流通信</strong>（如 <code>ip::tcp</code> 或本机流式 IPC <code>local::stream_protocol</code>）。开放 <code>connect</code>、<code>read_some</code> 和 <code>write_some</code> 接口。</p>
</li>
<li>
<p dir="auto"><strong><code>DatagramSocket</code>（数据报套接字）</strong>：<br />
继承自 <code>BaseSocket</code>。代表<strong>无连接、不可靠的数据报通信</strong>（如 <code>ip::udp</code>）。无 <code>connect</code> 语义，仅开放 <code>send_to</code> 和 <code>receive_from</code> 接口。</p>
</li>
</ol>
<p dir="auto">通过这一分层，若业务代码试图在 UDP Socket 上调用 <code>accept</code>，编译器将在编译阶段直接抛出“找不到该成员函数”的错误。我们将潜在的运行时崩溃彻底转化为编译期约束。同时，该架构也为未来扩充不同的协议栈保留了正交性。</p>
<h3>2. 契约先行：Protocol 的 Concept 约束</h3>
<p dir="auto">在泛型编程中，必须确保传入的模板参数是合法的协议类型。根据上文对 TCP 的设计，协议需要提供调用 <code>::socket()</code> 系统调用所需的三要素：<code>domain</code>、<code>type</code> 和 <code>protocol</code>。</p>
<p dir="auto">我们使用 C++20 的 Concept 来定义这一显式契约：</p>
<pre><code class="language-cpp">#include &lt;concepts&gt;

// src/socket.h
template &lt;typename T&gt;
concept has_domain = requires (const T&amp; t) { 
    { t.domain() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept has_type = requires (const T&amp; t) { 
    { t.type() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept has_protocol = requires (const T&amp; t) {
    { t.protocol() } -&gt; std::convertible_to&lt;int&gt;;
};

template &lt;typename T&gt;
concept socket_protocol = has_domain&lt;T&gt; &amp;&amp; has_type&lt;T&gt; &amp;&amp; has_protocol&lt;T&gt;;
</code></pre>
<p dir="auto">引入 <code>socket_protocol</code> 约束后，任何不满足规范的自定义协议类型在实例化 Socket 时，编译器都会提供精确的诊断信息。</p>
<h3>3. 第一步：RAII 资源基类 BaseSocket</h3>
<p dir="auto">接下来构建套接字的基类 <code>BaseSocket</code>。</p>
<p dir="auto">其核心是实现严格的所有权（Ownership）和移动语义。在系统级编程中，文件描述符是独占资源。尽管可以通过 <code>dup</code> 复制文件描述符，但在基础套接字封装中，拷贝往往意味着所有权语义的混乱。因此，我们明确拒绝拷贝语义。</p>
<pre><code class="language-cpp">#include &lt;system_error&gt;
#include &lt;utility&gt;
#include &lt;unistd.h&gt;
#include "exceptions.h"

template&lt;socket_protocol Protocol, typename Context&gt;
class BaseSocket {
public:
    using context_type = Context;
    using protocol_type = Protocol;

    // 1. 基于协议类型创建底层 fd
    BaseSocket(Context&amp; context, const Protocol&amp; protocol)
      : context_{ &amp;context }, 
        fd_{ create(protocol) }
    {}

    // 2. 彻底禁用拷贝语义
    BaseSocket(const BaseSocket&amp;) = delete;
    auto operator=(const BaseSocket&amp;) -&gt; BaseSocket&amp; = delete;

    // 3. 完美的移动语义：交接控制权，并将源对象的 fd 置为无效
    BaseSocket(BaseSocket&amp;&amp; other) noexcept
      : context_{ std::exchange(other.context_, nullptr) }, 
        fd_{ std::exchange(other.fd_, INVALID_SOCKET) }
    {}

    auto operator=(BaseSocket&amp;&amp; other) noexcept -&gt; BaseSocket&amp;
    {
        if (this == &amp;other) return *this;
        close(); // 覆盖前必须先关闭自己现有的 fd
        context_ = std::exchange(other.context_, nullptr);
        fd_ = std::exchange(other.fd_, INVALID_SOCKET);
        return *this;
    }

    // 4. RAII 析构
    virtual ~BaseSocket() 
    {
        close();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto is_valid() const noexcept -&gt; bool 
    {
        return fd_ != INVALID_SOCKET;
    }

    auto close() noexcept -&gt; std::expected&lt;void, std::error_code&gt; 
    {
        if (is_valid()) {
            auto res = ::close(fd_);
            fd_ = INVALID_SOCKET;
            if (res == -1) return unexpected_system_error();
        }
        return {};
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto native_handle() const noexcept -&gt; int { return fd_; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto context() noexcept -&gt; Context&amp; { return *context_; }

protected:
    // 供 Acceptor 接收新连接后直接接管已存在的 fd
    BaseSocket(Context&amp; context, int fd)
      : context_{ &amp;context }, fd_{ fd }
    {}

private:
    static constexpr int INVALID_SOCKET = -1;
    Context* context_;
    int fd_ = INVALID_SOCKET;

    static auto create(const Protocol&amp; protocol) -&gt; int 
    {
        auto res = ::socket(protocol.domain(), protocol.type(), protocol.protocol());
        if (res == -1) throw_system_error("Failed to create socket");
        return res;
    }
};
</code></pre>
<p dir="auto">这里模板参数的声明顺序（<code>Protocol</code>, <code>Context</code>）具有其实际意义。通过让 <code>Context</code> 作为第二个模板参数，我们可以在提供 <code>Protocol</code> 的前提下，利用 C++17 的类模板参数推导（CTAD）简化客户端代码：</p>
<pre><code class="language-cpp">// 固定协议别名
template&lt;typename Context&gt;
using socket = stream_socket&lt;tcp, Context&gt;;

IOContext context{};

// 编译器自动推导 Context 为 IOContext，无需显式指定模板参数
auto s = socket{ context };
</code></pre>
<h3>3.5 端点查询：CRTP + ADL 的组合拳</h3>
<p dir="auto">fd 有了，自然想知道自己绑在哪个端口、连的是谁——<code>getsockname</code> 和 <code>getpeername</code> 就是干这个的。</p>
<p dir="auto">最直觉的做法是给 <code>BaseSocket</code> 加两个成员函数。但 <code>getpeername</code> 要求 socket 先 <code>connect()</code> 过，Acceptor 根本不 <code>connect()</code>，放成员函数上就是把一个"调了必然失败"的接口暴露给所有人。况且查询地址和资源管理本来就是两件事，没必要耦合在一起。</p>
<p dir="auto">我们这里换个思路：把查询逻辑做成自由函数，用 CRTP 注入，让 ADL 负责查找。</p>
<pre><code class="language-cpp">template&lt;typename Derived&gt;
struct QueryLocalEndpoint {
    friend auto local_endpoint(const Derived&amp; socket) noexcept
        requires QuerableSocket&lt;Derived&gt;
    {
        return operations::query_local_endpoint&lt;typename Derived::endpoint_type&gt;(
            socket.native_handle());
    }
};
</code></pre>
<p dir="auto"><code>friend</code> 写在类模板体内，但它是一个自由函数——调用 <code>local_endpoint(socket)</code> 时，编译器通过 ADL 在 <code>socket</code> 的关联命名空间里找到它。<code>BaseSocket</code> 继承这个 CRTP，<code>StreamSocket</code>、<code>DatagramSocket</code>、<code>Acceptor</code> 全部继承 <code>BaseSocket</code>，一行代码，全部覆盖。</p>
<p dir="auto"><code>QueryRemoteEndpoint</code> 就不能这样了：</p>
<pre><code class="language-cpp">template&lt;typename Protocol&gt;
class StreamSocket: public BaseSocket&lt;Protocol&gt;,
                    public QueryRemoteEndpoint&lt;StreamSocket&lt;Protocol&gt;&gt; { ... };
</code></pre>
<p dir="auto">只在有 <code>connect()</code> 的子类上单独挂。在 Acceptor 上调用 <code>remote_endpoint</code> 编译能过，但运行时必然返回 <code>ENOTCONN</code>——不如直接不提供，错误在类型层面就消失了。</p>
<pre><code class="language-cpp">// Acceptor：绑定端口 0 后查内核实际分配的端口
if (auto ep = local_endpoint(acceptor))
    log::info("Listening on {}", *ep);  // 0.0.0.0:12345

// StreamSocket：connect 后一眼看清两端
if (auto local = local_endpoint(socket), remote = remote_endpoint(socket); local &amp;&amp; remote)
    log::info("Connected: {} -&gt; {}", *local, *remote);  // 192.168.1.2:54321 -&gt; 1.2.3.4:443
</code></pre>
<h3>4. 第二步：构建面向连接的 StreamSocket</h3>
<p dir="auto">确立了资源管理基线后，我们可以派生出 <code>StreamSocket</code>。</p>
<p dir="auto">流式套接字的核心特征在于点对点连接（提供 <code>connect</code>）及无边界的字节流传输（提供 <code>read_some</code> 和 <code>write_some</code>）。这里我们摒弃了传统的 <code>void* buffer</code> 配合 <code>size_t length</code>，全面使用 C++20 的 <code>std::span&lt;std::byte&gt;</code>。它携带连续内存的边界信息，能在编译期和运行期最大限度地避免内存越界。</p>
<pre><code class="language-cpp">#ifndef BLOG_IP_STREAM_SOCKET_H
#define BLOG_IP_STREAM_SOCKET_H

#include &lt;span&gt;
#include &lt;expected&gt;
#include "socket.h"
#include "operations.h"
#include "readsome_awaiter.h"   // 协程 Awaiter
#include "writesome_awaiter.h"

namespace ip {

template&lt;typename Protocol, typename Context&gt;
class StreamSocket: public BaseSocket&lt;Protocol, Context&gt; {
public:
    using base_socket_type = BaseSocket&lt;Protocol, Context&gt;;
    using endpoint_type = typename Protocol::endpoint;

    explicit StreamSocket(Context&amp; context)
      : base_socket_type{ context, Protocol{} }
    {}

    // 接收内核已完成的连接 (用于 Acceptor 生成)
    StreamSocket(Context&amp; context, int fd)
      : base_socket_type{ context, fd }
    {}

    StreamSocket(StreamSocket&amp;&amp;) = default;
    StreamSocket&amp; operator=(StreamSocket&amp;&amp;) = default;

    ~StreamSocket() = default;

    // --- 同步阻塞接口 ---
    void connect(const endpoint_type&amp; peer) 
    {
        // 配合 Endpoint 的 data() 和 size() 语义
        operations::connect(this-&gt;native_handle(), peer.data(), peer.size());
    }

    auto read_some(std::span&lt;std::byte&gt; buffer) noexcept 
        -&gt; std::expected&lt;std::size_t, std::error_code&gt; 
    {
        return operations::read_some(this-&gt;native_handle(), buffer);
    }

    auto write_some(std::span&lt;const std::byte&gt; buffer) noexcept 
        -&gt; std::expected&lt;std::size_t, std::error_code&gt; 
    {
        return operations::write_some(this-&gt;native_handle(), buffer);
    }

    // --- 异步协程接口 (对接 io_uring) ---
    auto async_read_some(std::span&lt;std::byte&gt; buffer) noexcept 
        -&gt; ReadSomeAwaiter&lt;Context&gt; 
    {
        return ReadSomeAwaiter&lt;Context&gt;{ this-&gt;context(), this-&gt;native_handle(), buffer };
    }

    auto async_write_some(std::span&lt;const std::byte&gt; buffer) noexcept 
        -&gt; WriteSomeAwaiter&lt;Context&gt; 
    {
        return WriteSomeAwaiter&lt;Context&gt;{ this-&gt;context(), this-&gt;native_handle(), buffer };
    }
};

} // namespace ip
#endif // BLOG_IP_STREAM_SOCKET_H
</code></pre>
<p dir="auto">通过继承，<code>StreamSocket</code> 天然获取了生命周期管理能力，仅需专注具体的 I/O 逻辑。</p>
<p dir="auto">值得探讨的是，<strong>为何 <code>StreamSocket</code> 位于 <code>namespace ip</code> 中？</strong></p>
<p dir="auto">在纯抽象层面，流式套接字似乎应是一个全局泛型概念：只要能读写字节流，即为 Stream Socket。然而，底层协议之间天然存在物理特征的不兼容。例如，IP 协议栈的流式套接字拥有专属于 IP 层的控制选项（如控制 Nagle 算法的 <code>TCP_NODELAY</code>）。</p>
<p dir="auto">如果将其强制抽象为全局通用的 <code>StreamSocket</code>，会导致这些特有配置选项失去编译期类型保护，进而增加运行时决议的复杂度和错误风险。因此，利用 <code>namespace ip</code> 进行物理与语义双重隔离，是维持零开销抽象的必要设计：</p>
<pre><code class="language-cpp">namespace ip {
template&lt;typename Protocol, typename Context&gt;
class StreamSocket: public BaseSocket&lt;Protocol, Context&gt; {
public:
    // ...
    // IP 协议簇专属的类型安全选项
    using keep_alive_idle = ValueOption&lt;IPPROTO_TCP, TCP_KEEPIDLE&gt;;
    using keep_alive_interval = ValueOption&lt;IPPROTO_TCP, TCP_KEEPINTVL&gt;;
    using keep_alive_count = ValueOption&lt;IPPROTO_TCP, TCP_KEEPCNT&gt;;
    using no_delay = BooleanOption&lt;IPPROTO_TCP, TCP_NODELAY&gt;;
    // ...
};
}
</code></pre>
<h4>缓冲区适配：优雅的调用体验 (Ergonomics)</h4>
<p dir="auto">与此同时，我们必须正视一个工程体验问题：虽然将底层的 I/O 接口固化为 <code>std::span&lt;std::byte&gt;</code> 确保了绝对的内存边界安全，但这会给上层业务代码带来不适。我们显然不能强制用户在每次读写时，都笨拙地手动强转指针，或者仅仅为了网络传输而将所有业务层容器都声明为 <code>std::array&lt;std::byte, N&gt;</code>。</p>
<p dir="auto">为了优化调用侧的体验，同时不向底层 I/O 方法中引入任何多余的模板复杂度，我们利用 C++20 的 Concept，提供一个轻量级的视图转换工厂函数 <code>buffer()</code>。</p>
<p dir="auto">对于<strong>只读</strong>的连续内存容器，我们将其零开销转换为 <code>std::span&lt;const std::byte&gt;</code>，用于 <code>write_some</code>：</p>
<pre><code class="language-cpp">template&lt;std::ranges::contiguous_range T&gt;
auto buffer(const T&amp; range) noexcept -&gt; std::span&lt;const std::byte&gt;
{
    return std::as_bytes(std::span{ range });
}
</code></pre>
<p dir="auto">这样一来，用户可以顺畅地写出如下代码，而不必亲自干预指针转换：</p>
<pre><code class="language-cpp">std::string msg = "Hello, io_uring!";
// 自动推导并转换，安全且零拷贝
client_sock.write_some(buffer(msg));
</code></pre>
<p dir="auto">对于<strong>可写</strong>的连续内存容器，我们将其转换为 <code>std::span&lt;std::byte&gt;</code>，用于 <code>read_some</code> 这样的输出调用：</p>
<pre><code class="language-cpp">template&lt;std::ranges::contiguous_range T&gt;
    requires (!std::is_const_v&lt;std::remove_reference_t&lt;std::ranges::range_reference_t&lt;T&gt;&gt;&gt;)
auto buffer(T&amp; range) noexcept -&gt; std::span&lt;std::byte&gt;
{
    return std::as_writable_bytes(std::span{ range });
}
</code></pre>
<p dir="auto">同样地，读取操作也变得极其自然：</p>
<pre><code class="language-cpp">std::vector&lt;char&gt; recv_buf(1024);
// 安全地提取底层连续内存块供内核填充
client_sock.read_some(buffer(recv_buf));
</code></pre>
<p dir="auto">这一层薄薄的抽象，大大优化了调用体验，且没有在核心的 I/O 方法中引入多余的模板膨胀。</p>
<h3>5. 协议的装配：Type Traits 工厂</h3>
<p dir="auto">如何将泛型的 <code>StreamSocket</code> 与具体的协议（如 TCP）优雅绑定？</p>
<p dir="auto">在我们的设计中，<code>Protocol</code> 类（例如 <code>ip::tcp</code>）不仅提供静态常量，还充当类型特征（Type Traits）的装配枢纽。</p>
<p dir="auto">将上述组件装配进去：</p>
<pre><code class="language-cpp">namespace ip {

class tcp {
public:
    // 强制约束 tcp 的 socket 实现类型
    template&lt;typename Context&gt;
    using socket = StreamSocket&lt;tcp, Context&gt;;
    
    // ... 其他静态特征 ...
};

} // namespace ip
</code></pre>
<h3>结语：零开销与强类型的交响曲</h3>
<p dir="auto">至此，让我们审视业务代码的最终形态：</p>
<pre><code class="language-cpp">ip::tcp::socket&lt;IOContext&gt; client{ context };
ip::tcp::endpoint target = ip::tcp::endpoint::from_string("127.0.0.1", 8080);

client.connect(target);

std::vector&lt;char&gt; recv_buf(1024);
client.read_some(buffer(recv_buf))
</code></pre>
<p dir="auto">在这寥寥数行代码中，类型系统在幕后默默完成了以下推导与约束：</p>
<ol>
<li><code>ip::tcp::socket</code> 自动推导为 <code>StreamSocket&lt;ip::tcp, IOContext&gt;</code>。</li>
<li>编译器确保 <code>connect</code> 仅接受 <code>ip::tcp::endpoint</code>，如果误传 <code>udp::endpoint</code> 会导致编译立刻失败。</li>
<li><code>StreamSocket</code> 的基类构造函数静态提取 <code>ip::tcp</code> 的 <code>SOCK_STREAM</code> 和 <code>IPPROTO_TCP</code> 以发起安全的系统调用。</li>
<li>对象离开作用域时，<code>BaseSocket</code> 的 RAII 机制确保文件描述符被安全释放。</li>
</ol>
<p dir="auto">我们彻底隐藏了底层的状态机转移与裸露的 C API，以层次分明、类型严格的现代 C++ 接口取而代之。由于高度模板化和内联优化，这一系列抽象的运行期开销，严格等价于手写裸 C 语言代码。</p>
<p dir="auto">在下一篇文章中，我们将补齐拼图的最后一块：<code>Acceptor</code>（被动接收器）的封装。届时，便可利用这套基础设施，跑通完整的基于 <code>io_uring</code> 的全异步协程服务器。</p>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/tree/main/demo/src/ip" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/194/基于-io_uring-的-c-20-协程网络库-06-socket层次化封装</link><guid isPermaLink="true">http://forum.d2learn.org/topic/194/基于-io_uring-的-c-20-协程网络库-06-socket层次化封装</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】05 Protocol与Endpoint的封装]]></title><description><![CDATA[<p dir="auto">在构建了底层的异步 I/O 引擎（<code>IOContext</code>）与核心的 Awaiter 机制后，我们的网络库已经具备了处理并发事件的能力。但要让它真正与网络世界通信，我们必须跨越网络编程的第一道门槛：<strong>地址与端点（Endpoint）的封装</strong>。</p>
<p dir="auto">在原生的 POSIX C API 中，网络地址的表示极其繁琐。开发者需要手动处理 <code>sockaddr_in</code>（IPv4）、<code>sockaddr_in6</code>（IPv6）甚至 <code>sockaddr_un</code>（Unix Domain Socket），并充斥着各种宏与危险的指针强制转换。</p>
<p dir="auto">本章的目标是：从零开始，一步步利用 C++20 的语言特性，构建出一套强类型、内存安全且<strong>零运行时开销</strong>的 Endpoint 体系。（也可以看做对asio的cosplay）</p>
<p dir="auto">我们最终期望的业务层 API 形态，应该是极致简洁的：</p>
<pre><code class="language-cpp">// 期望的现代 C++ 用法：自动推导，透明解析
auto ep1 = ip::tcp::endpoint::from_string("127.0.0.1", 12345); 
auto ep2 = ip::tcp::endpoint::from_string("::1", 12345);       
</code></pre>
<p dir="auto">要实现这一目标，我们需要拆解三个核心概念：协议（Protocol）、IP 地址（Address）与端点（Endpoint）。</p>
<hr />
<h3>1. 协议抽象：静态特征与运行期状态的权衡</h3>
<p dir="auto">任何网络通信都需要指定协议。在 C++ 中，为了追求性能，我们通常倾向于将一切可能的信息在编译期固化。对于 <code>Protocol</code>，第一时间，我们可能会设计出一个如下的纯模板类：</p>
<pre><code class="language-cpp">// 纯静态协议封装（存在局限性）
template&lt;int Domain, int Type, int Protocol&gt;
struct BasicProtocol {
    static constexpr int domain = Domain;
    static constexpr int type = Type;
    static constexpr int protocol = Protocol;
};
</code></pre>
<p dir="auto">对于 <code>Type</code>（如 <code>SOCK_STREAM</code> 代表 TCP）和 <code>Protocol</code>（如 <code>IPPROTO_TCP</code>），它们确实是静态不变的。然而，<code>Domain</code>（地址族，即 <code>AF_INET</code> 或 <code>AF_INET6</code>）在现代网络编程中，<strong>并非总能在编译期绝对固化</strong>。</p>
<p dir="auto">考虑一个支持 <strong>双栈(Dual-Stack)</strong> 的服务器：当你创建一个监听 <code>::</code> 的 Acceptor 时，它在运行期需要同时处理 IPv4 和 IPv6 的接入。如果 <code>Domain</code> 被彻底写死在模板参数中，我们将无法用单一的泛型类型来描述这个 Acceptor。</p>
<p dir="auto">因此，正确的设计哲学是：<strong>固化协议的本原特征，保留地址族的运行期决议能力</strong>。</p>
<p dir="auto">以下是我们对 <code>ip::tcp</code> 的完整实现：</p>
<pre><code class="language-cpp">namespace ip {
class tcp {
public:
    // 1. 本原特征：使用 consteval 强制在编译期求值，拒绝任何运行时状态的介入
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; consteval auto type() const noexcept -&gt; int { return SOCK_STREAM; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; consteval auto protocol() const noexcept -&gt; int { return IPPROTO_TCP; }

    // 2. 运行期特征：使用 constexpr，允许在运行时根据双栈需求进行动态切换
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto domain() const noexcept -&gt; int { return domain_; }

    // 具名构造器，语义清晰
    static auto v4() noexcept -&gt; tcp { return tcp{ AF_INET }; }
    static auto v6() noexcept -&gt; tcp { return tcp{ AF_INET6 }; }

private:
    int domain_ = AF_INET;
    explicit tcp(int domain) : domain_{ domain } {}
};
} // namespace ip
</code></pre>
<p dir="auto">通过将 <code>type()</code> 和 <code>protocol()</code> 声明为 <code>consteval</code>，我们在编译器层面建立了一条严格的契约，这在后续作为 Type Traits 提取协议参数时，提供了与静态常量完全一致的零开销保证。</p>
<h3>2. IP 地址封装</h3>
<p dir="auto">端点是由 IP 地址和端口组成的。在封装端点之前，我们需要先解决繁琐的 IP 地址解析。</p>
<p dir="auto">在 POSIX 中，IPv4 被存储为 4 字节的 <code>in_addr</code>，IPv6 被存储为 16 字节的 <code>in6_addr</code>。</p>
<p dir="auto">我们首先利用 RAII 将它们分别封装，并隐藏丑陋的 <code>inet_pton</code>（字符串转网络字节序）系统调用。</p>
<p dir="auto">以 <code>AddressV4</code> 为例：</p>
<pre><code class="language-cpp">struct AddressV4 {
    using address_type = in_addr;
    address_type address{};

    // 字符串解析工厂
    static auto from_string(std::string_view address) -&gt; AddressV4 {
        AddressV4 result;
        if (::inet_pton(AF_INET, address.data(), &amp;result.address) != 1)
            throw_system_error("Failed to convert string to IPv4 address");
        return result;
    }

    // 格式化输出
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto to_string() const -&gt; std::string {
        std::string buffer(INET_ADDRSTRLEN, '\0');
        if (::inet_ntop(AF_INET, &amp;address, buffer.data(), INET_ADDRSTRLEN) == nullptr)
            throw_system_error("Failed to convert IPv4 address to string");
        return buffer;
    }
};
// AddressV6 的实现高度对称，此处省略
</code></pre>
<p dir="auto"><strong>统一抽象：构建 Address 类</strong></p>
<p dir="auto">在业务代码中，我们不希望用户去手动 <code>if/else</code> 判断当前字符串是 V4 还是 V6。我们需要一个统一的 <code>Address</code> 类来容纳它们。</p>
<p dir="auto">此时，<strong><code>std::variant</code></strong> 成为了最完美的工具。我们可以给出如下实现：</p>
<pre><code class="language-cpp">class Address {
public:
    using address_type = std::variant&lt;AddressV4, AddressV6&gt;;

    Address() = default;
    Address(const AddressV4&amp; ipv4) : address_{ ipv4 } {}
    Address(const AddressV6&amp; ipv6) : address_{ ipv6 } {}

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto is_v4() const noexcept -&gt; bool {
        return std::holds_alternative&lt;AddressV4&gt;(address_);
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto to_string() const -&gt; std::string {
        // 优雅的多态调用
        return std::visit([](const auto&amp; addr) { return addr.to_string(); }, address_);
    }

    static auto from_string(std::string_view address) -&gt; Address {
        AddressV4 ipv4{};
        if (::inet_pton(AF_INET, address.data(), &amp;ipv4.address) == 1)
            return { ipv4 };

        AddressV6 ipv6{};
        if (::inet_pton(AF_INET6, address.data(), &amp;ipv6.address) == 1)
            return { ipv6 };

        throw_system_error("Invalid IP address format");
        std::unreachable();
    }

private:
    address_type address_;
};
</code></pre>
<p dir="auto">通过 <code>from_string</code>，我们实现了一个健壮的解析器：它会依次尝试按 IPv4 和 IPv6 解析字符串，并将成功的结果打包进安全的 <code>variant</code> 容器中。</p>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/src/ip/address.h" rel="nofollow ugc">Address完整代码</a></p>
<h3>3. Endpoint 封装：跨越 ABI 边界的内存博弈</h3>
<p dir="auto">现在，我们来到了最核心的部件 <code>BasicEndpoint</code>。它需要将前面实现的 <code>Protocol</code>、<code>Address</code> 与端口（Port）结合起来，并最终生成底层的 <code>sockaddr</code> 结构供内核使用。</p>
<h4>3.1 为什么引入 Protocol 模板参数？</h4>
<p dir="auto">你可能会疑惑：既然 IP 层的底层表示都是 <code>sockaddr</code>，为什么我们要设计成模板类 <code>BasicEndpoint&lt;Protocol&gt;</code>，而不是一个通用的 <code>Endpoint</code> 类？</p>
<p dir="auto">这是出于 <strong>强类型安全</strong> 的考量。<br />
TCP 的 <code>127.0.0.1:80</code> 和 UDP 的 <code>127.0.0.1:80</code> 在底层字节上完全一致，但在物理逻辑上是截然不同的通道。如果它们是同一个类型，开发者极易将 UDP 的端点传给 TCP 的 Socket 进行 <code>connect</code>，这种谬误只能在运行时由内核抛出异常。<br />
通过 <code>Protocol</code> 模板，<code>endpoint&lt;tcp&gt;</code> 和 <code>endpoint&lt;udp&gt;</code> 在 C++ 编译器眼中变成了绝对正交的两种类型，任何混用都会在编译期被拦截，这是零开销抽象的典范。</p>
<h4>3.2 致命陷阱：为何 Endpoint 内部必须摒弃 std::variant？</h4>
<p dir="auto">在封装 <code>Address</code> 时，我们使用了 <code>std::variant</code>。但在 <code>Endpoint</code> 内部存储底层结构时，却不能继续使用 <code>std::variant&lt;sockaddr_in, sockaddr_in6&gt;</code> 了，</p>
<p dir="auto"><code>Endpoint</code> 的内存不仅用于读取，更要以裸指针（<code>sockaddr*</code>）的形式交给内核 API（如 <code>accept</code> 或 <code>recvfrom</code>）。这些 API 具有 <strong>Overwrite</strong> 语义：内核会直接根据实际接收到的连接，向这块内存灌入 IPv4 或 IPv6 的字节流。</p>
<p dir="auto">如果底层是 <code>std::variant</code>：</p>
<ol>
<li><strong>内存布局破坏</strong>：<code>variant</code> 内部存在一个用于记录当前类型的 <code>index</code> 标记（以及可能的对齐 Padding）。内核如果从首地址开始写 <code>sa_family</code>，会直接破坏这个标记。</li>
<li><strong>状态脱节（UB）</strong>：假设 <code>variant</code> 当前为 IPv4（16字节），内核写入了 IPv6（28字节）的数据。内核无从知晓 C++ 的机制，绝不会去更新 <code>variant</code> 的 <code>index</code>。当 C++ 代码再次读取时，将发生严重的未定义行为（Undefined Behavior）。</li>
</ol>
<h4>3.3 解决方案：Union 与 sockaddr_storage</h4>
<p dir="auto">为了在确保 C 兼容性的同时提供 C++ 视图，最标准的解决方案是：<strong>使用 <code>union</code> 配合 <code>sockaddr_storage</code>。</strong> 也借此机会，强调一下C++的底层哲学：<strong>程序的世界没有银弹</strong>。对于不同场景选择适合的方式，所以C++提供了大量特性。</p>
<pre><code class="language-cpp">template&lt;typename Protocol&gt;
class BasicEndpoint {
public:
    using protocol_type = Protocol;
    using address_type = Address;

    // ... 构造函数见下文 ...

    // 提供给内核 API 的多态强转接口
    auto data() noexcept -&gt; sockaddr* 
    {
        return reinterpret_cast&lt;sockaddr*&gt;(&amp;data_.storage);
    }

private:
    // 经典的多态内存视图 (Aliased Views)
    union AddressType {
        sockaddr_storage storage; // 128字节，最严格对齐，提供绝对安全的物理容量兜底
        sockaddr_in v4;           // 提供给 C++ 侧的 IPv4 具象化读写视图
        sockaddr_in6 v6;          // 提供给 C++ 侧的 IPv6 具象化读写视图
    } data_;
};
</code></pre>
<p dir="auto">这里巧妙利用了 C/C++ 语言规范中的<strong>公共初始序列</strong>机制：所有 <code>sockaddr</code> 家族结构体的头两个字节都是 <code>sa_family_t</code>。</p>
<p dir="auto">因此，即便内核粗暴地覆盖了 <code>storage</code>，我们依然可以通过 <code>data_.storage.ss_family</code> 安全且合法地获知当前内存中实际装载的协议。</p>
<h3>4. 严守边界：Size 与 Capacity 的严格隔离</h3>
<p dir="auto">底层封装中最容易触发“缓冲区截断”Bug 的，是如何向内核报告这块 <code>union</code> 的长度。我们必须显式隔离 <code>size()</code> 和 <code>capacity()</code> 的语义：</p>
<pre><code class="language-cpp">    // 专供 输出型 API (如 accept, recvfrom) 使用
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto capacity() const noexcept -&gt; socklen_t {
        return sizeof(sockaddr_storage); // 永远返回最大容量 128 字节
    }

    // 专供 输入型 API (如 bind, connect) 使用
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto size() const noexcept -&gt; socklen_t {
        if (data_.storage.ss_family == AF_INET) return sizeof(sockaddr_in); // 16 字节
        return sizeof(sockaddr_in6); // 28 字节
    }
</code></pre>
<ul>
<li><strong>Input 操作（<code>bind</code> / <code>connect</code>）</strong>：内核要求<strong>精确匹配</strong>。如果你调用 <code>bind</code> 时传入了 128 字节（<code>capacity</code>），内核会因为长度不符合 IPv4（16）或 IPv6（28）的规约而直接返回 <code>-EINVAL</code>。必须严格使用动态计算的 <code>size()</code>。</li>
<li><strong>Output 操作（<code>accept</code>）</strong>：内核要求提供<strong>最大安全缓冲</strong>。在双栈监听模式下，随时可能接入 28 字节的 IPv6 客户端。如果此时你传入的是 <code>size()</code>（若端点默认初始化为 v4，则为 16），内核会直接截断写入，导致提取到的客户端地址完全损坏。必须严格使用 <code>capacity()</code>。</li>
</ul>
<h3>5. 拼图闭环：优雅的构造过程</h3>
<p dir="auto">有了上述坚实的底层基础，我们可以将 <code>Address</code> 对象转换为底层的 <code>union</code> 表示，最终兑现文章开头“一键构造”的承诺：</p>
<pre><code class="language-cpp">    BasicEndpoint(const address_type&amp; address, in_port_t port) {
        std::memset(&amp;data_, 0, sizeof(data_)); // 彻底清空，防止残留垃圾数据
        
        if (address.is_v4()) {
            data_.storage.ss_family = AF_INET;
            data_.v4.sin_family = AF_INET;
            data_.v4.sin_port = ::htons(port);
            data_.v4.sin_addr = address.to_v4().address;
        } else {
            data_.storage.ss_family = AF_INET6;
            data_.v6.sin6_family = AF_INET6;
            data_.v6.sin6_port = ::htons(port);
            data_.v6.sin6_addr = address.to_v6().address;
        }
    }

    static auto from_string(std::string_view address, in_port_t port) -&gt; BasicEndpoint {
        // 委托给 Address 的 variant 解析引擎，解析完成后交由本类的构造函数填充物理内存
        return BasicEndpoint{ address_type::from_string(address), port };
    }
</code></pre>
<p dir="auto">至此，我们的 <code>Endpoint</code> 彻底打通了从上层字符串抽象到底层 C 语言裸内存的通路。它对外提供了严格的类型契约，对内完美化解了 ABI 边界的内存博弈。</p>
<p dir="auto">对了，不要忘了，在tcp中，导出我们的endpoint</p>
<pre><code class="language-c++">class tcp {
public:
    using address = Address;

    using endpoint = BasicEndpoint&lt;tcp&gt;;
};
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/src/ip/endpoint.h" rel="nofollow ugc">Endpoint完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/193/基于-io_uring-的-c-20-协程网络库-05-protocol与endpoint的封装</link><guid isPermaLink="true">http://forum.d2learn.org/topic/193/基于-io_uring-的-c-20-协程网络库-05-protocol与endpoint的封装</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】04 核心IO操作的协程实现]]></title><description><![CDATA[<p dir="auto">终于，我们的基础设施已经足以支撑起真正的网络交互，是时候着手实现 <code>async_read</code>、<code>async_write</code> 以及 <code>async_accept</code> 等核心操作了。</p>
<p dir="auto">在本文中，我们会以一个相对简练的 <code>Socket</code> 封装作为承载这些操作的起点。这显然不是它的最终形态（我们的最终目标是实现类似 Boost.Asio 的现代化 API 结构），但如果一上来就陷入对底层套接字选项、地址解析等细节的繁琐封装中，将严重偏离探讨协程并发模型的主线。因此，第一版的 <code>Socket</code> 仅是一个将底层系统调用简单捏合的产物。</p>
<p dir="auto">本文的焦点将完全聚集于底层的协程流转机制，以及我们如何通过确立公开的 Concept 契约，赋予网络库极其强大的组合与扩展能力。</p>
<hr />
<h3>1. 过渡期的 Socket 封装</h3>
<p dir="auto">第一版的 <code>Socket</code> 核心职责是基于 RAII 管理文件描述符的生命周期，并利用 C++ 模板将底层的 <code>setsockopt</code> 等操作强类型化，消灭裸露的 <code>void*</code> 强转。</p>
<p dir="auto">在错误处理策略上，我们确立了一个清晰的工程边界：</p>
<ul>
<li><strong>初始化与配置阶段</strong>（如 <code>socket</code> 创建、<code>bind</code>、<code>listen</code>、<code>setsockopt</code>）：这些操作如果失败，通常意味着系统资源耗尽或配置存在致命错误，因此直接抛出异常（Throw Exception）。</li>
<li><strong>网络 I/O 交互阶段</strong>（如 <code>read</code>、<code>write</code>、<code>accept</code>）：在分布式系统中，超时、对端重置连接等属于常规的运行时分支，不应引发代价高昂的栈展开（Stack Unwinding）。因此，这部分操作必须返回 <code>std::expected&lt;T, std::error_code&gt;</code>。</li>
</ul>
<p dir="auto">基础代码如下：</p>
<pre><code class="language-cpp">#include &lt;sys/socket.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;utility&gt;
#include &lt;span&gt;

template&lt;typename Context&gt;
class Socket {
public:
    using context_type = Context;

    using reuse_address = BooleanOption&lt;SOL_SOCKET, SO_REUSEADDR&gt;;
#ifdef SO_REUSEPORT
    using reuse_port = BooleanOption&lt;SOL_SOCKET, SO_REUSEPORT&gt;;
#endif
    using error = BooleanOption&lt;SOL_SOCKET, SO_ERROR&gt;;
    using receive_buffer_size = ValueOption&lt;SOL_SOCKET, SO_RCVBUF&gt;;
    using send_buffer_size = ValueOption&lt;SOL_SOCKET, SO_SNDBUF&gt;;
    using non_blocking = FlagOption&lt;F_GETFL, F_SETFL, O_NONBLOCK&gt;;
    using close_on_exec = FlagOption&lt;F_GETFD, F_SETFD, FD_CLOEXEC&gt;;

    Socket(Context&amp; context, int domain, int type, int protocol)
      : context_{ context }
    {
        fd_ = ::socket(domain, type, protocol);
        if (fd_ == -1)
            throw_system_error("Failed to create socket");
    }

    Socket(Context&amp; context, int fd)
      : context_{ context }, 
        fd_(fd) 
    {}

    Socket(const Socket&amp;) = delete;
    auto operator=(const Socket&amp;) -&gt; Socket&amp; = delete;

    Socket(Socket&amp;&amp; other) noexcept
      : context_{ other.context_ }, 
        fd_{ std::exchange(other.fd_, -1) }
    {}

    auto operator=(Socket&amp;&amp; other) noexcept -&gt; Socket&amp;
    {
        if (this != &amp;other) {
            close();
            context_ = other.context_;
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    ~Socket()
    {
        close();
    }

    template&lt;socket_option Option&gt;
    void option(const Option&amp; value)
    {
        auto res = ::setsockopt(fd_, Option::level, Option::name, value.data(), value.size());
        if (res == -1)
            throw_system_error("Failed to set socket option[{}]", Option::name);
    }

    template&lt;socket_option Option&gt;
    auto option() const -&gt; Option
    {
        Option option{};
        auto size = static_cast&lt;socklen_t&gt;(option.size());
        auto res = ::getsockopt(fd_, Option::level, Option::name, option.data(), &amp;size);
        if (res == -1)
            throw_system_error("Failed to get socket option[{}]", Option::name);

        if (size != option.size())
            throw std::runtime_error{ "Unexpected socket option size" };

        return option;
    }

    template&lt;flag_option Option&gt;
    void option(const Option&amp; value)
    {
        auto current_flags = ::fcntl(fd_, Option::get_cmd);
        if (current_flags == -1)
            throw_system_error("Failed to get socket flags");

        auto new_flags = value ? (current_flags | Option::bit) : (current_flags &amp; ~Option::bit);
        if (::fcntl(fd_, Option::set_cmd, new_flags) == -1)
            throw_system_error("Failed to set socket flags");
    }

    template&lt;flag_option Option&gt;
    auto option() const -&gt; Option
    {
        auto current_flags = ::fcntl(fd_, Option::get_cmd);
        if (current_flags == -1)
            throw_system_error("Failed to get socket flags");

        if (current_flags &amp; Option::bit)
            return Option{ true };
        
        return Option{ false };
    }

    void close()
    {
        if (fd_ != -1) {
            ::close(fd_);
            fd_ = -1;
        }
    }

    constexpr auto native_handle() const -&gt; int
    {
        return fd_;
    }

    void bind(const sockaddr* addr, socklen_t addrlen)
    {
        if (::bind(fd_, addr, addrlen) == -1)
            throw_system_error("Failed to bind socket");
    }

    void listen(int backlog)
    {
        if (::listen(fd_, backlog) == -1)
            throw_system_error("Failed to listen on socket");
    }

    // 前置声明的协程 I/O 操作接口
    auto async_accept() -&gt; AcceptAwaiter&lt;Socket&lt;Context&gt;&gt;;
    auto async_readsome(std::span&lt;std::byte&gt; buffer) -&gt; ReadSomeAwaiter&lt;Context&gt;;
    auto async_writesome(std::span&lt;const std::byte&gt; buffer) -&gt; WriteSomeAwaiter&lt;Context&gt;;

private:
    Context&amp; context_;
    int fd_;
};
</code></pre>
<h3>2. 核心 Awaiter 剖析</h3>
<p dir="auto">在实现底层 Awaiter 时，我们需要遵循上一节确立的 <code>single_shot_only_operation</code> 概念约束，确保它们能够与 <code>TimeoutAwaiter</code> 无缝协作。</p>
<h4>2.1 ReadSomeAwaiter</h4>
<p dir="auto">在传统的 C 语言接口中，<code>read</code> 通常使用 <code>void* buffer</code> 配合 <code>size_t len</code>。而在现代 C++ 中，我们应当使用视图类型 <code>std::span&lt;std::byte&gt;</code>。这不仅明确了字节流的意图，更在编译期消除了指针越界的风险。</p>
<p dir="auto">更重要的是，由于协程挂起期间，Awaiter 对象驻留在独立的协程帧中，外部传入的 <code>buffer</code> 视图会在整个异步操作期间保持有效。这从语言特性层面保证了内存安全，使得我们无需引入额外的引用计数机制。</p>
<pre><code class="language-cpp">#ifndef BLOG_READSOME_AWAITER_H
#define BLOG_READSOME_AWAITER_H

#include &lt;coroutine&gt;
#include &lt;cstddef&gt;
#include &lt;expected&gt;
#include &lt;span&gt;
#include &lt;system_error&gt;
#include &lt;utility&gt;

#include &lt;liburing.h&gt;

#include "exceptions.h"
#include "operation.h"

template&lt;typename Context&gt;
class ReadSomeAwaiter : public Operation {
public:
    using resume_type = std::size_t;

    ReadSomeAwaiter(Context&amp; context, int fd, std::span&lt;std::byte&gt; buffer)
      : context_{ context }, fd_{ fd }, buffer_{ buffer }
    {}

    constexpr auto await_ready() const noexcept -&gt; bool
    {
        return false;
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;

        auto* sqe = context_.sqe();

        prepare(sqe);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;resume_type, std::error_code&gt;
    {
        if (error_code_ != 0)
            return unexpected_system_error(error_code_);

        return byte_read_;
    }

    void prepare(::io_uring_sqe* sqe) noexcept
    {
        ::io_uring_prep_recv(sqe, fd_, buffer_.data(), buffer_.size(), 0);
    }

    void set_result(int result, std::uint32_t flags) noexcept
    {
        if (result &gt;= 0)
            byte_read_ = static_cast&lt;std::size_t&gt;(result);
        else
            error_code_ = -result;
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

    auto context() noexcept -&gt; Context&amp; { return context_; }

private:
    Context&amp; context_;
    int fd_;
    std::span&lt;std::byte&gt; buffer_;

    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    std::size_t byte_read_{ 0 };
    int error_code_{ 0 };
};

#endif // BLOG_READSOME_AWAITER_H
</code></pre>
<p dir="auto">在 <code>Socket</code> 中，我们只需将其作为返回值工厂抛出：</p>
<pre><code class="language-cpp">auto async_readsome(std::span&lt;std::byte&gt; buffer) -&gt; ReadSomeAwaiter&lt;Context&gt;
{
    return ReadSomeAwaiter&lt;Context&gt;{ context_, fd_, buffer };
}
</code></pre>
<h4>2.2 WriteSomeAwaiter</h4>
<p dir="auto">写操作 <code>WriteSomeAwaiter</code> 与读操作高度对称。唯一的区别是它接受只读视图 <code>std::span&lt;const std::byte&gt;</code>，并将其底层操作码映射至 <code>io_uring_prep_send</code>。</p>
<pre><code class="language-cpp">#ifndef BLOG_WRITESOME_AWAITER_H
#define BLOG_WRITESOME_AWAITER_H

#include &lt;coroutine&gt;
#include &lt;cstddef&gt;
#include &lt;expected&gt;
#include &lt;span&gt;
#include &lt;utility&gt;

#include &lt;liburing.h&gt;

#include "exceptions.h"
#include "operation.h"

template&lt;typename Context&gt;
class WriteSomeAwaiter : public Operation {
public:
    using resume_type = std::size_t;

    WriteSomeAwaiter(Context&amp; context, int fd, std::span&lt;const std::byte&gt; buffer)
      : context_{ context }, fd_{ fd }, buffer_{ buffer }
    {}

    constexpr auto await_ready() const noexcept -&gt; bool
    {
        return false;
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;

        auto* sqe = context_.sqe();
        
        prepare(sqe);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;resume_type, std::error_code&gt;
    {
        if (error_code_ != 0)
            return unexpected_system_error(error_code_);

        return byte_written_;
    }

    void prepare(::io_uring_sqe* sqe) noexcept
    {
        ::io_uring_prep_send(sqe, fd_, buffer_.data(), buffer_.size(), 0);
    }

    void set_result(int result, std::uint32_t flags) noexcept
    {
        if (result &gt;= 0)
            byte_written_ = static_cast&lt;std::size_t&gt;(result);
        else
            error_code_ = -result;
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

    auto context() noexcept -&gt; Context&amp; { return context_; }

private:
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    Context&amp; context_;
    int fd_;
    std::span&lt;const std::byte&gt; buffer_;
    std::size_t byte_written_{ 0 };
    int error_code_{ 0 };
};

#endif // BLOG_WRITESOME_AWAITER_H
</code></pre>
<p dir="auto"><code>Socket</code> 中的接口实现：</p>
<pre><code class="language-cpp">auto async_writesome(std::span&lt;const std::byte&gt; buffer) -&gt; WriteSomeAwaiter&lt;Context&gt;
{
    return WriteSomeAwaiter&lt;Context&gt;{ context_, fd_, buffer };
}
</code></pre>
<h4>2.3 AcceptAwaiter</h4>
<p dir="auto"><code>AcceptAwaiter</code> 负责处理被动连接接入。在原生 C 接口中，<code>accept</code> 可以通过传入 <code>sockaddr*</code> 来获取对端地址信息。由于当前版本的实现并未对地址（Endpoint）进行体系化封装，该版本的 <code>AcceptAwaiter</code> 暂不支持提取连接地址，而是直接移交内核产生的新 <code>fd</code>，由其自动推导包装为新的 <code>Socket</code> 实例返回。</p>
<pre><code class="language-cpp">#ifndef BLOG_ACCEPT_AWAITER_H
#define BLOG_ACCEPT_AWAITER_H

#include &lt;coroutine&gt;
#include &lt;expected&gt;
#include &lt;utility&gt;

#include &lt;liburing.h&gt;

#include "exceptions.h"
#include "operation.h"

template&lt;typename Socket&gt;
class AcceptAwaiter : public Operation {
public:
    using context_type = typename Socket::context_type;
    using socket_type = Socket;
    using resume_type = socket_type;

    AcceptAwaiter(context_type&amp; context, int fd)
      : context_{ context }, fd_{ fd }
    {} 

    constexpr auto await_ready() const noexcept -&gt; bool
    {
        return false;
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;

        auto* sqe = context_.sqe();
        
        prepare(sqe);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;resume_type, std::error_code&gt;
    {
        if (error_code_ != 0)
            return unexpected_system_error(error_code_);

        return resume_type{ context_, result_fd_ };
    }

    void prepare(::io_uring_sqe* sqe) noexcept
    {
        ::io_uring_prep_accept(sqe, fd_, nullptr, nullptr, 0);
    }

    void set_result(int result, std::uint32_t flags) noexcept
    {
        if (result &gt;= 0)
            result_fd_ = result;
        else
            error_code_ = -result;
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

    auto context() noexcept -&gt; context_type&amp; { return context_; }

private:
    context_type&amp; context_;
    int fd_;

    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int result_fd_{ -1 };
    int error_code_{ 0 };
};

#endif // BLOG_ACCEPT_AWAITER_H
</code></pre>
<p dir="auto">在 <code>Socket</code> 中，挂载实现如下：</p>
<pre><code class="language-cpp">auto async_accept() -&gt; AcceptAwaiter&lt;Socket&lt;Context&gt;&gt;
{
    return AcceptAwaiter&lt;Socket&lt;Context&gt;&gt;{ context_, fd_ };
}
</code></pre>
<h3>3. Echo Server 实战</h3>
<p dir="auto">有了这三大基础 I/O Awaiter，我们已经可以构建一个极简但具备并发特征的 Echo Server了。</p>
<p dir="auto">在这个实现中，没有任何的回调嵌套，也没有跨线程的数据同步操作，控制流如同传统阻塞代码一般自上而下铺陈。</p>
<pre><code class="language-cpp">auto session(Socket&lt;IOContext&gt; client) -&gt; Task&lt;void&gt;
{
    std::array&lt;std::byte, 1024&gt; buffer{};

    while (true)
    {
        auto read_result = co_await client.async_readsome(buffer);
        if (!read_result) {
            spdlog::warn("Failed to read from client {}: {}", client.native_handle(), read_result.error().message());
            co_return;
        }

        auto bytes_read = *read_result;
        if (bytes_read == 0) {
            spdlog::info("Client {} disconnected", client.native_handle());
            co_return;
        }

        spdlog::info("Read {} bytes from client {}", bytes_read, client.native_handle());
        std::string_view data{ reinterpret_cast&lt;const char*&gt;(buffer.data()), bytes_read };
        spdlog::warn("Data from client {}: {}", client.native_handle(), data);

        auto write_buffer = std::span{ buffer }.first(bytes_read);
        auto write_result = co_await client.async_writesome(write_buffer);
        
        if (!write_result) {
            spdlog::warn("Failed to write to client {}: {}", client.native_handle(), write_result.error().message());
            co_return;
        }

        if (*write_result != bytes_read)
            spdlog::warn("Partial write on client {}: {} / {} bytes", client.native_handle(), *write_result, bytes_read);
    }
}

auto server(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    Socket acceptor{ context, AF_INET, SOCK_STREAM, 0 };
    acceptor.option(Socket&lt;IOContext&gt;::reuse_address{ true });

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = ::htons(12345);
    
    acceptor.bind(reinterpret_cast&lt;sockaddr*&gt;(&amp;addr), sizeof(addr));
    acceptor.listen(1024);

    while (true) {
        auto client = co_await acceptor.async_accept();
        if (!client) {
            spdlog::warn("Failed to accept connection: {}", client.error().message());
            continue;
        }

        co_spawn(context, session(std::move(*client)));
    }
}

auto shutdown_monitor(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;

    SignalSet sets{ context, signals::interrupt, signals::terminate };
    co_await sets.async_wait();

    spdlog::info("Received shutdown signal, stopping IOContext...");
    context.stop();
}

int main(int argc, char* argv[])
{
    IOContext context{};

    co_spawn(context, server(context));
    co_spawn(context, shutdown_monitor(context));

    context.run();

    spdlog::info("IOContext stopped, exiting...");
    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto">编译执行后，利用多个客户端建立连接，输出日志如下：</p>
<pre><code class="language-bash">blog.socket_v1
[2026-04-20 21:15:51.631] [info] Read 6 bytes from client 7
[2026-04-20 21:15:51.631] [warning] Data from client 7: dsfsa

[2026-04-20 21:15:57.312] [info] Read 23 bytes from client 8
[2026-04-20 21:15:57.312] [warning] Data from client 8: fdasfas的撒大法师

[2026-04-20 21:16:01.863] [info] Client 8 disconnected
[2026-04-20 21:16:03.021] [info] Client 7 disconnected
[2026-04-20 21:16:09.837] [info] Read 36 bytes from client 7
[2026-04-20 21:16:09.837] [warning] Data from client 7: asfasdfsasa打撒四方达dsa去玩

[2026-04-20 21:16:11.133] [info] Client 7 disconnected
^C[2026-04-20 21:16:14.162] [info] Received shutdown signal, stopping IOContext...
[2026-04-20 21:16:14.162] [info] IOContext stopped, exiting...
</code></pre>
<p dir="auto">更重要的是，得益于对实现中对 <code>single_shot_only_operation</code> 的支持，我们可以无需修改底层 <code>ReadSomeAwaiter</code> 的任何一行代码，直接将上一篇博客中构建的 <code>TimeoutAwaiter</code> 无缝挂载。</p>
<p dir="auto">针对读写超时的正交组合可以写出如下形式：</p>
<pre><code class="language-cpp">// 控制 read 超时
auto read_result = co_await timeout(client.async_readsome(buffer), 5s);
if (!read_result) {
    if (read_result.error() == std::errc::timed_out) {
        spdlog::debug("Read timed out on client {}", client.native_handle());
        continue;
    }

    spdlog::warn("Failed to read from client {}: {}", client.native_handle(), read_result.error().message());
    co_return;
}

// ... 处理接收逻辑 ...

// 控制 write 超时
auto write_result = co_await timeout(client.async_writesome(write_buffer), 5s);
if (!write_result) {
    if (write_result.error() == std::errc::timed_out) {
        spdlog::debug("Write timed out on client {}", client.native_handle());
        continue;
    }

    spdlog::warn("Failed to write to client {}: {}", client.native_handle(), write_result.error().message());
    co_return;
}
</code></pre>
<p dir="auto">基础骨架至此已完全跑通并形成闭环。在未来的优化中，我们将在这些底层基石之上，引入更完备的端点（Endpoint）表示与高级协议解析组件，让网络库从“可用”走向“现代化”。</p>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/socket_v1.cpp" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/192/基于-io_uring-的-c-20-协程网络库-04-核心io操作的协程实现</link><guid isPermaLink="true">http://forum.d2learn.org/topic/192/基于-io_uring-的-c-20-协程网络库-04-核心io操作的协程实现</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】03 基于链式请求的零开销超时机制]]></title><description><![CDATA[<p dir="auto">这一步要实现什么，我也很纠结。最好的当然是直奔socket的封装，然后实现async accept，async read，async write这些典型的协程调用。</p>
<p dir="auto">但是上文中留下一些坑又需要尽快填一下，否则后续再改会导致更大的重构。我们需要在这一篇中解释一下为什么要在 <code>PollAwaiter</code> 中开放set_result，prepare，context这些看起来毫无卵用的接口。</p>
<p dir="auto">最主要的目的就是能实现一个timeout接口。</p>
<p dir="auto">在传统的 Reactor 模型（如 epoll）中，实现超时通常需要在用户态维护一个独立的数据结构（如最小堆或时间轮）来管理定时器，这往往伴随着额外的动态内存分配（分配定时器节点）以及后台线程的唤醒开销。</p>
<p dir="auto"><code>io_uring</code> 提供了更底层的解法：<strong>链式请求（Linked Requests）</strong>。</p>
<p dir="auto">通过将 I/O 操作与定时器在内核态进行绑定，我们可以将状态同步的复杂度完全下沉至内核，从而实现真正的零动态分配超时机制。</p>
<h3>1. 接口约束：single_shot_only_operation</h3>
<p dir="auto">在实现通用的超时包装器之前，我们需要界定“什么类型的操作允许被包装”。</p>
<p dir="auto">我们定义 <code>single_shot_only_operation</code>，要求目标类型不仅继承自 <code>Operation</code> 基类，还必须提供用于获取上下文、组装 SQE 以及处理结果的特定接口：</p>
<pre><code class="language-cpp">#include &lt;concepts&gt;
#include &lt;coroutine&gt;
#include &lt;expected&gt;
#include &lt;system_error&gt;

template&lt;typename T&gt;
concept single_shot_only_operation = requires (T&amp; op, ::io_uring_sqe* sqe)
{
    typename T::resume_type;

    requires std::is_lvalue_reference_v&lt;decltype(op.context())&gt;;
    op.context();
    op.prepare(sqe);
    op.set_result(0, 0);
    { op.await_resume() } -&gt; std::same_as&lt;std::expected&lt;typename T::resume_type, std::error_code&gt;&gt;;
} &amp;&amp; std::derived_from&lt;T, Operation&gt;;
</code></pre>
<p dir="auto">基于此约束，无论底层的协程等待体是 <code>Socket::async_read</code> 还是 <code>SignalSet::async_wait</code>，只要满足条件，编译器即可保证其能被安全地赋予超时语义。</p>
<h3>2. 核心机制：IOSQE_IO_LINK 与内核竞速</h3>
<p dir="auto"><code>io_uring</code> 的 SQE 链式调用是我们实现零开销超时的核心。当我们在一个 SQE 的标志位中设置 <code>IOSQE_IO_LINK</code> 时，内核会将其与紧随其后提交的下一个 SQE 绑定为一个原子链。</p>
<p dir="auto">配合专用的操作码 <code>IORING_OP_LINK_TIMEOUT</code>，内核会执行以下竞速逻辑：</p>
<ol>
<li>内核并行处理业务 I/O 请求，并同时启动定时器。</li>
<li>如果业务请求先完成，内核自动取消挂载的定时器。</li>
<li>如果定时器先到期，内核自动强行取消业务请求，并使其返回 <code>-ECANCELED</code>。</li>
</ol>
<p dir="auto">这种将同步状态机交由内核仲裁的设计，使得用户态代码无需介入复杂的取消流程。</p>
<h3>3. TimeoutAwaiter 的内存安全陷阱与实现</h3>
<p dir="auto">构建 <code>TimeoutAwaiter</code> 时，面临的最大工程挑战是<strong>协程帧的生命周期管理</strong>。</p>
<p dir="auto">由于我们向内核一次性提交了两个 SQE（业务 I/O 与 Timeout），内核在执行完毕后，<strong>必然会返回两个对应的 CQE</strong>。如果采用“先到先得”的简单逻辑，在第一个 CQE 到达时立即调用 <code>handle.resume()</code> 恢复协程，会导致一个隐蔽且致命的 Use-After-Free (UAF) 漏洞：</p>
<p dir="auto">当协程被恢复后，包含在该协程帧内的 <code>TimeoutAwaiter</code> 对象可能会随着当前作用域的结束而立即析构。此时，内核中仍有一个被取消的 CQE 正在返回途中。当 <code>IOContext</code> 收割这个滞后的 CQE 并尝试调用 <code>complete</code> 时，其 <code>user_data</code> 指针已指向被释放的内存，导致进程崩溃。</p>
<p dir="auto">因此，必须在 <code>complete</code> 中引入一个无锁的计数器屏障，确保两个 CQE 全部落地后，再将控制权交还给协程。</p>
<p dir="auto">完整的 <code>TimeoutAwaiter</code> 实现如下：</p>
<pre><code class="language-cpp">template&lt;single_shot_only_operation InnerOperation&gt;
class TimeoutAwaiter : public Operation {
public:
    using resume_type = typename InnerOperation::resume_type;

    template&lt;typename Duration&gt;
    TimeoutAwaiter(InnerOperation&amp;&amp; operation, Duration timeout) noexcept
      : inner_operation_{ std::forward&lt;InnerOperation&gt;(operation) }
    {
        using namespace std::chrono;

        timeout_.tv_sec = duration_cast&lt;seconds&gt;(timeout).count();
        timeout_.tv_nsec = duration_cast&lt;nanoseconds&gt;(timeout % 1s).count();
    }

    constexpr auto await_ready() const noexcept -&gt; bool { return false; }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;

        auto* io_sqe = context().sqe();
        auto* timeout_sqe = context().sqe();

        // 组装业务 I/O 并设置链式标志
        inner_operation_.prepare(io_sqe);
        io_sqe-&gt;flags |= IOSQE_IO_LINK;
        ::io_uring_sqe_set_data(io_sqe, this);

        // 紧跟超时探测请求
        ::io_uring_prep_link_timeout(timeout_sqe, &amp;timeout_, 0);
        ::io_uring_sqe_set_data(timeout_sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;resume_type, std::error_code&gt;
    {
        if (is_timed_out_)
            return unexpected_system_error(std::errc::timed_out);

        inner_operation_.set_result(result_, 0);
        return inner_operation_.await_resume();
    }

    void set_result(int result, std::uint32_t flags) noexcept
    {
        if (result == -ETIME)
            is_timed_out_ = true;
        else if (result != -ECANCELED)
            result_ = result;
    }

    void complete(int result, std::uint32_t flags) noexcept override
    {
        set_result(result, flags);

        if (--pending_cqes_ == 0) {
            auto handle = std::exchange(handle_, {});
            handle.resume();
        }
    }
    
    auto context() noexcept -&gt; decltype(std::declval&lt;InnerOperation&amp;&gt;().context())
    {
        return inner_operation_.context();
    }

private:
    InnerOperation inner_operation_;
    struct __kernel_timespec timeout_{};

    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    
    int pending_cqes_{ 2 }; // 提交了 2 个 SQE，必然返回 2 个 CQE
    bool is_timed_out_{ false };
    int result_{ -ECANCELED };
};

template&lt;single_shot_only_operation Operation, typename Duration&gt;
auto timeout(Operation&amp;&amp; awaitable, Duration t) noexcept -&gt; TimeoutAwaiter&lt;std::decay_t&lt;Operation&gt;&gt;
{
    return TimeoutAwaiter&lt;std::decay_t&lt;Operation&gt;&gt;{ std::forward&lt;Operation&gt;(awaitable), t };
}
</code></pre>
<h4>架构收益：Core-Per-Thread 带来的无锁抽象</h4>
<p dir="auto">细心的读者可能会发现，在处理跨越不同异步回调的生命周期同步时，我们仅仅使用了一个普通的内建整型变量 <code>int pending_cqes_{ 2 };</code>，而没有求助于 <code>std::atomic&lt;int&gt;</code> 或任何形式的互斥锁。</p>
<p dir="auto">这正是我们选择 <strong>Core-Per-Thread（单线程独立上下文）</strong> 架构的直接收益。在该模型下，底层 <code>io_uring</code> 队列的投递、事件的收割（<code>IOContext::run</code>）、<code>complete</code> 回调的触发，以及协程的恢复，全都被严格限制在单一线程的顺序执行流中。这种确定的串行化特征从根本上消除了数据竞争。因此，我们可以毫无顾忌地使用裸整型进行状态流转，彻底免除了原子操作带来的缓存行同步与内存屏障开销，将“零开销抽象”贯彻到了每一个微小的细节中。</p>
<h3>4. 正交设计：避免 API 表面积爆炸</h3>
<p dir="auto">在上述实现中，<code>std::expected</code> 发挥了重要作用，我们将内核传回的 <code>-ETIME</code> 翻译为了标准的 <code>std::errc::timed_out</code>。但比类型安全更值得关注的，是这种基于泛型与组合语义带来的高层架构美学。</p>
<p dir="auto">在传统的网络库设计中，超时逻辑往往被直接硬编码进具体的 I/O 操作中。这意味着设计者不得不提供诸如 <code>async_read_with_timeout</code>、<code>async_write_with_timeout</code>、<code>async_connect_with_timeout</code> 等一系列冗余接口。假设系统存在 $N$ 种基础操作，未来又需要引入 $M$ 种类似于超时的修饰语义，API 的数量就会呈现 $N \times M$ 的指数级膨胀，最终导致<strong>表面积爆炸（API Surface Area Explosion）</strong>。</p>
<p dir="auto">而我们设计的 <code>TimeoutAwaiter</code> 与任何具体的业务操作是<strong>严格正交</strong>的。通过 C++20 的 Concept 约束，它充当了一个纯粹的通用修饰器，能够无缝叠加在任何满足规范的操作之上，将库的 API 复杂度完美控制在了 $N + M$。</p>
<p dir="auto">结合泛型的工厂函数，上层业务代码可以以极低侵入性的自然语序组合它们：</p>
<pre><code class="language-cpp">auto network_read_task(IOContext&amp; context, Socket&amp; socket) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    
    // 组合语义：以正交的方式为 async_read 附加 5 秒的超时约束
    auto result = co_await timeout(socket.async_read(buffer), 5s);
    
    if (!result) {
        if (result.error() == std::errc::timed_out)
            spdlog::warn("Read operation timed out.");
        else
            spdlog::error("Read failed: {}", result.error().message());
            
        co_return;
    }
    
    spdlog::info("Successfully read {} bytes.", result.value());
}
</code></pre>
<h3>演示</h3>
<p dir="auto">由于现有实现比较简陋，我们只能复用下02章中async_wait来测试下timeout的语义是否正确。实际的代码里，是不太可能组合async_wait和timeout的。</p>
<pre><code class="language-c++">
auto shutdown_monitor(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;

    SignalSet sets{ context, signals::interrupt, signals::terminate };

    // 唯一改动点，测试一下timeout的行为是否正确
    co_await timeout(sets.async_wait(), 5s);

    spdlog::info("Received shutdown signal, stopping IOContext...");
    context.stop();
}

auto demo(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    spdlog::info("demo started");    

    // 模拟一些持续的异步工作，直到接收到退出信号
    while (true) 
        co_await sleep_for(context, 1s);

    spdlog::info("demo completed");
}

int main(int argc, char* argv[])
{
    IOContext context{};

    co_spawn(context, demo(context));
    co_spawn(context, shutdown_monitor(context));

    context.run();

    spdlog::info("IOContext stopped, exiting...");

    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto">执行结果</p>
<pre><code class="language-bash">[2026-04-20 02:28:23.356] [info] demo started
[2026-04-20 02:28:28.792] [info] Received shutdown signal, stopping IOContext...
[2026-04-20 02:28:28.792] [info] IOContext stopped, exiting...
</code></pre>
<p dir="auto">可以看到，5s之后，async_wait结束了等待，context.stop()触发，程序结束了。</p>
<p dir="auto">也可以按下Ctrl+C来触发SIGINT信号</p>
<pre><code class="language-bash">/home/doom/blog/build/demo/blog.timeout_v1
[2026-04-20 10:25:32.885] [info] demo started
^C[2026-04-20 10:25:33.891] [info] Received shutdown signal, stopping IOContext...
[2026-04-20 10:25:33.891] [info] IOContext stopped, exiting...
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/timeout_v1.cpp" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/191/基于-io_uring-的-c-20-协程网络库-03-基于链式请求的零开销超时机制</link><guid isPermaLink="true">http://forum.d2learn.org/topic/191/基于-io_uring-的-c-20-协程网络库-03-基于链式请求的零开销超时机制</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】02：模块解耦与完备的退出机制]]></title><description><![CDATA[<p dir="auto">在上文中，我们构建了 <code>IOContext</code> 的核心事件循环骨架。然而，随着组件的增加，我们需要解决两个实际的工程问题：</p>
<p dir="auto">一是如何对底层上下文进行合理的抽象与解耦；</p>
<p dir="auto">二是如何优雅、无阻塞地处理外部中断信号并终止事件循环。</p>
<h3>1. 模块解耦与泛型化设计</h3>
<p dir="auto">考虑到未来我们可能会迭代出多个版本的 <code>IOContext</code>，为了最大化代码复用，将具体的协程 Awaiter（如 <code>SleepAwaiter</code>）与底层的 <code>IOContext</code> 实现解耦是必要的。</p>
<p dir="auto">首先，我们将 <code>Operation</code> 接口提取到独立的头文件中。</p>
<p dir="auto">其次，对于 <code>SleepAwaiter</code>，由于它依赖 <code>Context::sqe()</code>，若在头文件中硬编码 <code>IOContext</code>，必须要立刻知道IOContext的定义。因此，我们采用模板化设计，将 <code>Context</code> 泛型化，在调用点推导出确切的类型。</p>
<pre><code class="language-cpp">template&lt;typename Context&gt;
class SleepAwaiter: public Operation {
public:
    template&lt;chrono_duration Duration&gt;
    SleepAwaiter(Context&amp; context, Duration d)
      : context_{ context }
    {
        using namespace std::chrono;
        // 转换 std::chrono 时间为内核认识的 timespec
        timeout_.tv_sec = duration_cast&lt;seconds&gt;(d).count();
        timeout_.tv_nsec = duration_cast&lt;nanoseconds&gt;(d % seconds(1)).count();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    constexpr auto await_ready() const noexcept -&gt; bool 
    { 
        return false; 
    }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;
        auto* sqe = context_.sqe();

        // 提交纯超时指令，count 设为 0 表示只受时间触发
        ::io_uring_prep_timeout(sqe, &amp;timeout_, 0, 0);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() noexcept -&gt; std::expected&lt;void, std::error_code&gt;
    {
        // io_uring 中，超时正常结束会返回 ETIME
        if (error_code_ == ETIME || error_code_ == 0)
            return {};
        
        // 其他错误（如 ECANCELED 被提前强杀）
        return unexpected_system_error(error_code_);
    }

    void complete(int res, std::uint32_t flags) noexcept override
    {
        error_code_ = -res;
        
        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

private:
    Context&amp; context_;
    struct __kernel_timespec timeout_{};
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int error_code_{ 0 };
};

template&lt;typename Context, typename Duration&gt;
auto sleep_for(Context&amp; context, Duration duration) noexcept -&gt; SleepAwaiter&lt;Context&gt;
{
    return SleepAwaiter&lt;Context&gt;{ context, duration };
}
</code></pre>
<p dir="auto"><strong>设计注记</strong>：<br />
通常情况下，过度泛型化（滥用模板）会劣化编译时长，并不值得推崇。但在基础设施库的设计中，静态多态（基于模板的 Duck Typing）能够做到零运行时开销（Zero-overhead），且调用方代码无需任何修改即可适配不同版本的 <code>IOContext</code>，这种妥协是极具工程价值的。</p>
<h3>2. 完善事件循环的终止机制 (eventfd)</h3>
<p dir="auto">上个版本中，我们使用 <code>std::atomic&lt;bool&gt; should_stop_</code> 标志来控制循环退出。但这存在一个死锁隐患：如果 <code>IOContext::run()</code> 正阻塞在 <code>io_uring_submit_and_wait</code> 系统调用上，单纯修改布尔变量是无法唤醒内核态线程的。</p>
<p dir="auto">我们需要一种跨越内核与用户态的唤醒机制。在传统的 Reactor 模式中，通常采用管道（pipe）或 <code>eventfd</code>，在 <code>io_uring</code> 体系下，<code>eventfd</code> 依然是开销极小且最适用的方案。</p>
<h4>核心状态重构：区分系统事件与业务逻辑</h4>
<p dir="auto">在引入 <code>eventfd</code> 后，完成队列（CQ）中不仅会包含业务逻辑的事件（如网络 I/O、定时器），还会混入我们内部触发的 wakeup 事件。<br />
这就要求我们必须在状态追踪上做出严格区分：</p>
<ol>
<li>
<p dir="auto"><strong><code>count</code></strong>：追踪当前批次取出的所有 CQE 数量，用于向前推进内核的共享环形缓冲区（<code>io_uring_cq_advance</code>）。</p>
</li>
<li>
<p dir="auto"><strong><code>workdone</code></strong>：追踪实际完成的业务任务数量，仅针对这些任务去扣减 <code>outstanding_works_</code>。如果不对二者加以区分，wakeup 信号会导致业务计数器异常递减，引发程序提前退出或触发断言失败。</p>
</li>
</ol>
<p dir="auto">同时，我们利用 liburing 原生的内联辅助函数 <code>io_uring_cqe_get_data64</code>、<code>io_uring_cqe_get_data</code> 以及 <code>io_uring_sqe_set_data64</code> 来取代底层的直接字段访问，这消除了 <code>reinterpret_cast</code> 的滥用，确保了类型安全的边界。</p>
<p dir="auto">完整的 <code>IOContext</code> 实现如下：</p>
<pre><code class="language-cpp">#include &lt;sys/eventfd.h&gt;
#include &lt;unistd.h&gt;
#include &lt;limits&gt;
#include &lt;cassert&gt;

class IOContext {
public:
    explicit IOContext(unsigned entries = 1024)
    {
        if (auto res = ::io_uring_queue_init(entries, &amp;ring_, 0); res &lt; 0)
            throw_system_error(-res, "io_uring_queue_init");            

        wakeup_fd_ = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
        if (wakeup_fd_ == -1)
            throw_system_error("Failed to create eventfd for stopping IOContext");

        arm_wakeup();
    }

    IOContext(const IOContext&amp;) = delete;
    auto operator=(const IOContext&amp;) -&gt; IOContext&amp; = delete;
    
    // 为了简化实现，我们不支持移动
    IOContext(IOContext&amp;&amp; other) noexcept = delete;
    auto operator=(IOContext&amp;&amp;) -&gt; IOContext&amp; = delete;

    ~IOContext()
    {
        ::io_uring_queue_exit(&amp;ring_);
        ::close(wakeup_fd_);
    }

    void run()
    {
        ::io_uring_cqe* cqe{ nullptr };

        while (!should_stop_.load(std::memory_order_relaxed) &amp;&amp; outstanding_works_ &gt; 0)
        {
            auto res = ::io_uring_submit_and_wait(&amp;ring_, 1);
            if (res &lt; 0) {
                if (res == -EINTR)
                    continue;

                throw_system_error("io_uring_submit_and_wait");
            }
                
            unsigned head;
            unsigned count{ 0 };
            unsigned workdone{ 0 };

            io_uring_for_each_cqe(&amp;ring_, head, cqe) {
                ++count;

                // 探测到内部唤醒信号
                if (io_uring_cqe_get_data64(cqe) == WAKEUP_MARKER) {
                    resume_wakeup();
                    arm_wakeup();
                    continue;
                }

                // 正常的业务逻辑完成事件
                if (io_uring_cqe_get_data64(cqe) != 0) {
                    auto* op = static_cast&lt;Operation*&gt;(io_uring_cqe_get_data(cqe));
                    op-&gt;complete(cqe-&gt;res, cqe-&gt;flags);

                    ++workdone;
                }
            }

            // 推进环形缓冲区必须使用总事件数 count
            if (count &gt; 0)
                ::io_uring_cq_advance(&amp;ring_, count);

            // 扣减未决任务必须使用实际完成的业务数 workdone
            if (workdone &gt; 0)
                outstanding_works_ -= workdone;
        }
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto sqe() -&gt; ::io_uring_sqe*
    {
        auto* sqe = ::io_uring_get_sqe(&amp;ring_);
        if (!sqe)
            throw_system_error("io_uring_get_sqe");

        add_work();
        return sqe;
    }

    void stop()
    {
        should_stop_.store(true, std::memory_order_relaxed);
        wakeup();
    }

    auto ring() noexcept -&gt; ::io_uring* { return &amp;ring_; }
    auto ring() const noexcept -&gt; const ::io_uring* { return &amp;ring_; }

    void add_work() noexcept { ++outstanding_works_; }

    void drop_work() noexcept
    {
        assert(outstanding_works_ &gt; 0);
        --outstanding_works_;
    }
    
private:
    static constexpr auto WAKEUP_MARKER = std::numeric_limits&lt;std::uintptr_t&gt;::max();

    ::io_uring ring_{};
    int wakeup_fd_{ -1 };

    // 只用来追踪 io_context 之外的操作，并不需要用户主动来使用相关的接口
    std::size_t outstanding_works_{ 0 };
    // stop 会被跨线程调用，所以需要使用原子变量来保证线程安全
    std::atomic&lt;bool&gt; should_stop_{ false };

    void arm_wakeup() noexcept
    {
        auto* sqe = ::io_uring_get_sqe(&amp;ring_);
        if (!sqe)
            throw_system_error("io_uring_get_sqe failed when re-arming wakeup");

        ::io_uring_prep_poll_add(sqe, wakeup_fd_, POLLIN);
        // 采用 64 位专有 setter，避免指针转换警告
        ::io_uring_sqe_set_data64(sqe, WAKEUP_MARKER);
    }   
    
    void wakeup()
    {
        std::uint64_t val = 1;
        ::write(wakeup_fd_, &amp;val, sizeof(val));
    }

    void resume_wakeup()
    {
        uint64_t val;
        ::read(wakeup_fd_, &amp;val, sizeof(val));
    }
};
</code></pre>
<p dir="auto"><strong>关于未处理完成的 CQE 的处置</strong>：<br />
触发 <code>stop()</code> 退出循环后，队列中如果还有积压的 CQE 怎么办？</p>
<p dir="auto">这正是 RAII 管理机制的优势所在。<code>IOContext</code> 的生命周期与系统资源严格绑定，当程序退出，<code>IOContext</code> 析构时，<code>io_uring_queue_exit</code> 会协同内核彻底销毁共享的环形缓冲区。由于事件循环已经终止，不会再有新的逻辑被触发，因此忽略未决的 CQE 是安全且合理的策略。</p>
<p dir="auto">如果用户确实有在停止后清理特定状态的需求，可以通过暴露的 <code>ring()</code> 接口自行干预。</p>
<h3>3. 基于 signalfd 的统一中断处理</h3>
<p dir="auto">既然已经实现了安全的唤醒与停止语义，顺理成章地，我们应将操作系统的信号（如 <code>SIGINT</code>, <code>SIGTERM</code>）也纳入异步框架。在 Linux 平台上，<code>signalfd</code> 提供了一种将异步中断转化为文件描述符可读事件的机制，它能被完美地集成进 <code>io_uring</code> 轮询模型中。</p>
<h4>3.1 强类型 Signal 封装</h4>
<p dir="auto">避免裸露的魔术整数：</p>
<pre><code class="language-cpp">class Signal {
public:
    explicit constexpr Signal(int signal) noexcept
      : signal_{ signal }
    {}

    auto operator==(const Signal&amp;) const noexcept -&gt; bool = default;
    // 这里的隐式转换是否提供看个人，我觉得不提供更好
    constexpr operator int() const noexcept { return signal_; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto value() const noexcept { return signal_; }

private:
    int signal_;
};

struct signals {
    signals() = delete;
    
    static constexpr auto interrupt = Signal{ SIGINT };
    static constexpr auto terminate = Signal{ SIGTERM };
    static constexpr auto quit = Signal{ SIGQUIT };
    static constexpr auto hangup = Signal{ SIGHUP };
};
</code></pre>
<h4>3.2 信号集 SignalSet 管理</h4>
<p dir="auto">使用折叠表达式优雅地处理变参掩码。另外，为保障跨线程时的健壮性，此处采用了标准的 <code>pthread_sigmask</code>。</p>
<pre><code class="language-cpp">#include &lt;sys/signalfd.h&gt;
#include &lt;signal.h&gt;

template&lt;typename Context&gt;
class SignalSet {
public:
    template&lt;typename... Signals&gt;
        requires (std::same_as&lt;Signals, Signal&gt; &amp;&amp; ...)
    SignalSet(Context&amp; io_context, Signals... signals)
      : io_context_{ io_context }
    {
        ::sigemptyset(&amp;mask_);
        (::sigaddset(&amp;mask_, signals.value()), ...);

        // 屏蔽这些信号的默认异步行为，交由 signalfd 同步读取
        if (::pthread_sigmask(SIG_BLOCK, &amp;mask_, nullptr) == -1)
            throw_system_error("Failed to block signals");

        fd_ = ::signalfd(-1, &amp;mask_, SFD_NONBLOCK | SFD_CLOEXEC);
        if (fd_ == -1)
            throw_system_error("Failed to create signalfd");
    }

    SignalSet(const SignalSet&amp;) = delete;
    auto operator=(const SignalSet&amp;) -&gt; SignalSet&amp; = delete;

    SignalSet(SignalSet&amp;&amp; other) noexcept
      : io_context_{ other.io_context_ }, 
        fd_{ std::exchange(other.fd_, -1) },
        mask_{ other.mask_ }
    {}

    auto operator=(SignalSet&amp;&amp; other) noexcept -&gt; SignalSet&amp; = delete;

    ~SignalSet()
    {
        if (fd_ != -1)
            ::close(fd_);
    }

private:
    Context&amp; io_context_;
    int fd_{ -1 };
    sigset_t mask_;
};
</code></pre>
<h4>3.3 构建 PollAwaiter 与 async_wait</h4>
<p dir="auto">既然是协程库，那我们理所应当的应该将监听行为设计成协程。为监听可读事件提供通用的 <code>PollAwaiter</code>：</p>
<pre><code class="language-cpp">template&lt;typename Context&gt;
class PollAwaiter : public Operation {
public:
    using resume_type = void;

    PollAwaiter(Context&amp; context, int fd, short events) noexcept
      : context_{ context }, 
        fd_{ fd }, 
        events_{ events }
    {}

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto await_ready() const noexcept -&gt; bool { return false; }

    auto await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept -&gt; void
    {
        handle_ = handle;
        auto* sqe = context_.sqe();
        
        ::io_uring_prep_poll_add(sqe, fd_, events_);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() const noexcept -&gt; std::expected&lt;void, std::error_code&gt;
    {
        if (error_code_ != 0)
            return unexpected_system_error(error_code_);
            
        return {};
    }

    void complete(int res, &lsqb;&lsqb;maybe_unused&rsqb;&rsqb; std::uint32_t flags) noexcept override
    {
        error_code_ = res &lt; 0 ? -res : 0;
        
        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

private:
    Context&amp; context_;
    int fd_;
    short events_;
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int error_code_{ 0 };
};

// 在 SignalSet 外部实现
template&lt;typename Context&gt;
auto SignalSet&lt;Context&gt;::async_wait() noexcept -&gt; PollAwaiter&lt;Context&gt;
{
    return PollAwaiter&lt;Context&gt;{ io_context_, fd_, POLLIN };
}   
</code></pre>
<h3>4. 系统集成演示</h3>
<p dir="auto">至此，基础组件均已就位。我们可以轻松写出一个支持非阻塞延时、并通过 <code>Ctrl+C</code> 信号安全、优雅退出的并发模型。</p>
<pre><code class="language-cpp">auto shutdown_monitor(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    SignalSet sets{ context, signals::interrupt, signals::terminate };

    // 挂起协程，等待操作系统向底层派发 SIGINT 或 SIGTERM
    co_await sets.async_wait();

    spdlog::info("Received shutdown signal, stopping IOContext...");
    context.stop();
}

auto demo(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    spdlog::info("before sleep...");    
    
    // 模拟长耗时异步任务
    co_await sleep_for(context, 10min);

    spdlog::info("after sleep...");
}

int main(int argc, char* argv[])
{
    IOContext context{};

    // 并发派发两个独立协程：一个执行业务，一个负责监听中断
    co_spawn(context, demo(context));
    co_spawn(context, shutdown_monitor(context));

    context.run();

    spdlog::info("IOContext stopped, exiting...");
    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto"><strong>运行结果</strong>：<br />
在长达 10 分钟的 <code>sleep</code> 任务途中，我们通过 <code>Ctrl+C</code> 触发键盘中断，<code>signalfd</code> 捕获到信号，唤醒了沉睡的 <code>shutdown_monitor</code> 协程，随后成功中断事件循环，程序安全退出。</p>
<pre><code class="language-bash">blog.io_context_v2
[2026-04-19 22:33:21.313] [info] before sleep...
^C[2026-04-19 22:33:22.954] [info] Received shutdown signal, stopping IOContext...
[2026-04-19 22:33:22.954] [info] IOContext stopped, exiting...
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/io_context_v2.cpp" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/190/基于-io_uring-的-c-20-协程网络库-02-模块解耦与完备的退出机制</link><guid isPermaLink="true">http://forum.d2learn.org/topic/190/基于-io_uring-的-c-20-协程网络库-02-模块解耦与完备的退出机制</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[【 基于 io_uring 的 C++20 协程网络库】01：基础骨架与 Awaiter 机制]]></title><description><![CDATA[<h3>目标与设计边界</h3>
<p dir="auto">本文旨在实现一个基于 <code>io_uring</code> 封装的 C++ 协程网络库。在着手编码前，我们先确立一个严格的设计边界：</p>
<p dir="auto"><strong>不考虑跨平台，不考虑兼容 epoll 等传统多路复用机制。</strong></p>
<p dir="auto">为什么舍弃跨平台等通用性？<br />
一旦引入跨平台封装，不仅维护成本陡增，更关键的是<strong>性能势必要做出妥协</strong>。不同操作系统的异步 API 在机制上存在根本分歧，强行封装通常只能取它们的公共子集，或者在用户态引入额外的抽象层来模拟缺失的语义。无论哪种方式，都会对最终性能造成不可预期的损耗。</p>
<p dir="auto">在基础设施级别的系统库中，性能是无法在项目后期通过“手法”来弥补的，必须在架构初期就定下基调。因此，我们选择不给自己埋雷，直接将底层与 <code>io_uring</code> 强绑定。</p>
<h3>为什么选择 io_uring？</h3>
<p dir="auto">相比于 epoll，<code>io_uring</code> 的心智模型更加契合协程。<br />
epoll 暴露的是 Reactor 模型接口（就绪通知），本质上依然是接近线程回调的处理方式。而 <code>io_uring</code> 是标准的 Proactor 模型（完成通知）。C++20 的协程天然就是一个异步操作状态机，也是标准的 Proactor 范式。两者的结合能最大程度地降低封装阻抗，减少无谓的状态转换代码。</p>
<h3>IOContext 是什么？</h3>
<p dir="auto">对于初接触异步网络库的读者，可以简单将 <code>IOContext</code> 理解为<strong>事件收割机与协程调度中枢</strong>。</p>
<p dir="auto">在代码中，你提交的各种异步操作（即 <code>io_uring</code> 的 SQE 事件），最终都需要一个统一的执行流去收割它们的完成结果（CQE）。成熟的模式是借鉴 Boost.Asio 的 <code>io_context</code> 抽象：通过阻塞调用 <code>IOContext::run()</code> 来消耗掉所有已就绪事件，唤醒对应的协程，然后继续等待下一轮事件就绪。</p>
<h3>构建基础框架</h3>
<p dir="auto">基于 C++ 的 RAII 原则，<code>IOContext</code> 的首要任务是管理 <code>io_uring</code> 实例的生命周期。</p>
<h4>1. 实例初始化</h4>
<pre><code class="language-cpp">int io_uring_queue_init(unsigned entries, struct io_uring* ring, unsigned flags);
</code></pre>
<ul>
<li><code>entries</code>: 提交队列（SQ）的深度。必须是 2 的幂（如 128, 256）。内核会基于此分配共享内存环。</li>
<li><code>ring</code>: 指向待初始化的实例。成功后，内存映射地址、队列掩码等状态将被写入该结构。</li>
<li><code>flags</code>: 控制行为的标志位（如启用 <code>SQPOLL</code> 消除系统调用）。我们这里默认置 0 即可。</li>
</ul>
<p dir="auto">失败时直接返回负值的系统错误码，不依赖全局 <code>errno</code>。</p>
<h4>2. 实例销毁</h4>
<pre><code class="language-cpp">void io_uring_queue_exit(struct io_uring* ring);
</code></pre>
<p dir="auto">该函数负责解除内存映射 (<code>munmap</code>)，并关闭 <code>io_uring</code> 在内核中对应的匿名文件描述符，防止虚拟内存与文件句柄泄漏。</p>
<h4>IOContext 资源管理骨架</h4>
<p dir="auto">基于上述 API，我们搭建出 <code>IOContext</code> 的核心轮廓。由于该上下文作为核心中枢运转，移动语义会引发悬垂指针等复杂问题，因此我们在设计上严格禁用拷贝与移动。</p>
<pre><code class="language-cpp">#include &lt;liburing.h&gt;
#include &lt;atomic&gt;

class IOContext {
public:
    explicit IOContext(unsigned entries)
    {
        if (auto res = ::io_uring_queue_init(entries, &amp;ring_, 0); res &lt; 0)
            throw_system_error(-res, "io_uring_queue_init");            
    }

    IOContext(const IOContext&amp;) = delete;
    auto operator=(const IOContext&amp;) -&gt; IOContext&amp; = delete;

    // 为了简化实现，我们不支持移动
    IOContext(IOContext&amp;&amp;) = delete;
    auto operator=(IOContext&amp;&amp;) -&gt; IOContext&amp; = delete;

    ~IOContext()
    {
        ::io_uring_queue_exit(&amp;ring_);
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto ring() noexcept -&gt; ::io_uring* { return &amp;ring_; }
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; auto ring() const noexcept -&gt; const ::io_uring* { return &amp;ring_; }

private:
    ::io_uring ring_;
};
</code></pre>
<h3>收割已就绪事件 (CQE)</h3>
<p dir="auto">接下来实现核心引擎 <code>IOContext::run()</code>。这涉及三个底层操作流：</p>
<ol>
<li>
<p dir="auto"><strong>等待事件就绪</strong>：<br />
<code>io_uring_submit_and_wait(struct io_uring* ring, unsigned wait_nr);</code></p>
<p dir="auto">将提交操作和阻塞等待融合成一次系统调用。<code>wait_nr</code> 指明线程必须阻塞到至少出现多少个完成事件才唤醒返回。</p>
<p dir="auto"><strong>返回值陷阱</strong>：成功时返回的是提交的 SQE 数量，而非完成的 CQE 数量。</p>
</li>
<li>
<p dir="auto"><strong>遍历完成队列 (CQ)</strong>：<br />
<code>io_uring_for_each_cqe</code> 是一个纯用户态宏。它通过带有 <em>Acquire</em> 语义的内存屏障读取 CQ 尾指针，无锁且零拷贝地遍历就绪事件。</p>
<p dir="auto"><strong>状态剥离陷阱</strong>：该宏只是只读遍历，不修改内核视角的头部指针。如果仅仅遍历而不推进状态，队列最终会溢出导致 <code>-EBUSY</code>。</p>
</li>
<li>
<p dir="auto"><strong>确认事件消费</strong>：<br />
<code>io_uring_cq_advance(struct io_uring* ring, unsigned nr);</code></p>
<p dir="auto">修改用户空间的 Head 指针，并通过 <em>Store-Release</em> 语义发布给内核，正式确认这些事件已被收割。</p>
</li>
</ol>
<h4>user_data 与类型擦除</h4>
<p dir="auto"><code>io_uring_cqe</code> 结构中包含一个 <code>__u64 user_data</code> 字段。当我们在 SQE 中设置它时，内核会原封不动地将其带入 CQE 返回。这使得我们能够将该标识强制转换回 C++ 对象的指针。</p>
<p dir="auto">为此，我们提供一个 <code>Operation</code> 基类，所有协程 Awaiter 都必须继承此接口：</p>
<pre><code class="language-cpp">struct Operation {
    virtual ~Operation() = default;
    virtual void complete(int res, unsigned flags) = 0;
};
</code></pre>
<h4>优雅的退出：should_stop_ 的无锁设计</h4>
<p dir="auto">为了安全退出事件循环，我们引入 <code>should_stop_</code> 变量。即便网络库采用 Core Per Thread 模型，不涉及跨业务线程的同步，但 <code>stop()</code> 操作往往是由操作系统的信号处理器（Signal Handler，如处理 Ctrl+C）触发的。信号中断具有强抢占性，因此必须使用 <code>std::atomic</code>。</p>
<p dir="auto">值得注意的是，这里我们<strong>不使用 CAS（Compare-And-Swap）</strong>。由于停止是一个幂等且无条件的覆盖动作，我们完全不关心过去的运行状态。直接使用 <code>store</code> 配合最松散的 <code>std::memory_order_relaxed</code> 即可。这提供了硬件级别的防数据撕裂保证，同时将同步开销降到了绝对的最低点。</p>
<blockquote>
<p dir="auto">WARN: 只有这个变量显然是不足以完整实现 stop 功能的，还需要考虑如何取消已经提交但尚未完成的 I/O 请求，以及如何通知正在等待的 run() 方法尽快返回。我们将在未来的版本中逐步完善这个功能。</p>
</blockquote>
<h4>完整的 run() 实现</h4>
<p dir="auto">结合外部任务追踪机制，事件循环的最终代码如下：</p>
<pre><code class="language-cpp">class IOContext {
    // ... 构造与析构保持不变 ...

    void run()
    {
        ::io_uring_cqe* cqe{ nullptr };

        while (!should_stop_.load(std::memory_order_relaxed) &amp;&amp; outstanding_works_ &gt; 0)
        {
            auto res = ::io_uring_submit_and_wait(&amp;ring_, 1);
            if (res &lt; 0)
                throw_system_error("io_uring_submit_and_wait");

            unsigned head;
            unsigned count{ 0 };

            io_uring_for_each_cqe(&amp;ring_, head, cqe) {
                ++count;

                if (cqe-&gt;user_data != 0) {
                    auto* op = reinterpret_cast&lt;Operation*&gt;(cqe-&gt;user_data);
                    op-&gt;complete(cqe-&gt;res, cqe-&gt;flags);
                }
            }

            if (count &gt; 0) {
                outstanding_works_ -= count;
                ::io_uring_cq_advance(&amp;ring_, count);
            }
        }
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; 
    auto sqe() -&gt; ::io_uring_sqe*
    {
        auto* sqe = ::io_uring_get_sqe(&amp;ring_);
        if (!sqe)
            xin::throw_system_error("io_uring_get_sqe");
        
        add_work();
        return sqe;
    }

    void stop() noexcept { should_stop_.store(true, std::memory_order_relaxed); }

    // 为了搭配co_spawn，需要暴露add_work和drop_work方法
    void add_work() noexcept { ++outstanding_works_; }

    void drop_work() noexcept
    {
        assert(outstanding_works_ &gt; 0);
        --outstanding_works_;
    }

private:
    ::io_uring ring_;
    std::size_t outstanding_works_{ 0 };
    std::atomic&lt;bool&gt; should_stop_{ false };
};
</code></pre>
<h3>深入 Awaiter 机制：SleepAwaiter 实践</h3>
<p dir="auto">单有一个 <code>IOContext</code> 是跑不起来的，我们需要验证它与 C++20 协程的交互机制。在此，我们实现一个 <code>SleepAwaiter</code>，封装 <code>io_uring</code> 的 <code>IORING_OP_TIMEOUT</code> 定时器。</p>
<pre><code class="language-cpp">#include &lt;chrono&gt;
#include &lt;coroutine&gt;
#include &lt;expected&gt;
#include &lt;system_error&gt;
#include &lt;utility&gt;

class SleepAwaiter : public Operation {
public:
    template&lt;typename Duration&gt;
    SleepAwaiter(IOContext&amp; context, Duration d) noexcept
      : context_{ context }
    {
        using namespace std::chrono;
        ts_.tv_sec = duration_cast&lt;seconds&gt;(d).count();
        ts_.tv_nsec = duration_cast&lt;nanoseconds&gt;(d % seconds(1)).count();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb; constexpr auto await_ready() const noexcept -&gt; bool { return false; }

    void await_suspend(std::coroutine_handle&lt;&gt; handle) noexcept
    {
        handle_ = handle;
        auto* sqe = context_.sqe();

        // 提交纯超时指令，count 设为 0 表示只受时间触发
        ::io_uring_prep_timeout(sqe, &amp;ts_, 0, 0);
        ::io_uring_sqe_set_data(sqe, this);
    }

    auto await_resume() const noexcept -&gt; std::expected&lt;void, std::error_code&gt;
    {
        // io_uring 中，超时正常结束会返回 ETIME
        if (error_code_ == ETIME || error_code_ == 0)
            return {};
        
        // 其他错误（如 -ECANCELED 被提前强杀）
        return std::unexpected{ std::error_code{ error_code_, std::generic_category() } };
    }

    void complete(int res, &lsqb;&lsqb;maybe_unused&rsqb;&rsqb; std::uint32_t flags) noexcept override
    {
        error_code_ = -res;

        if (handle_) {
            auto handle = std::exchange(handle_, nullptr);
            handle.resume();
        }
    }

private:
    IOContext&amp; context_;
    struct __kernel_timespec ts_{};
    std::coroutine_handle&lt;&gt; handle_{ nullptr };
    int error_code_{ 0 };
};

template&lt;typename Duration&gt;
auto sleep_for(IOContext&amp; context, Duration duration) noexcept -&gt; SleepAwaiter
{
    return SleepAwaiter{ context, duration };
}
</code></pre>
<h4>零开销生命周期管理</h4>
<p dir="auto">留意 <code>await_suspend</code> 中的 <code>::io_uring_prep_timeout(sqe, &amp;ts_, 0, 0);</code>。我们将局部对象 <code>ts_</code> 的地址交给了内核。在传统的异步回调编程中，这是一个极易触发悬垂指针的致命错误，通常需要用 <code>std::shared_ptr</code> 在堆上分配来强行续命。</p>
<p dir="auto">但在这里，它是<strong>绝对安全的</strong>。因为 <code>SleepAwaiter</code> 本身的生命周期被牢牢绑定在了协程帧内部。直到 <code>complete</code> 回调中触发 <code>handle.resume()</code> 彻底唤醒协程后，该 Awaiter 才会被销毁。协程从语言底层提供了天然的内存安全保障，这也是 C++ 追求零开销抽象的绝佳体现。</p>
<h4>测试示例</h4>
<p dir="auto">最后，我们用一段简单的代码来验证整个基建流转：</p>
<pre><code class="language-cpp">auto demo(IOContext&amp; context) -&gt; Task&lt;void&gt;
{
    using namespace std::chrono_literals;
    
    spdlog::info("before sleep");

    co_await sleep_for(context, 5s);

    spdlog::info("after sleep");
}

int main(int argc, char* argv[])
{
    IOContext context{};
    co_spawn(context, demo(context));

    context.run();
    return EXIT_SUCCESS;
}
</code></pre>
<p dir="auto">输出如下，可以看到，5s后再次输出内容，这证明从请求提交、内核响应、上下文分发到协程唤醒的全链路已完全贯通：</p>
<pre><code class="language-bash">blog.io_context_v1
[2026-04-19 16:18:17.642] [info] before sleep...
[2026-04-19 16:18:22.642] [info] after sleep...
</code></pre>
<p dir="auto"><a href="https://github.com/Doomjustin/blog/blob/main/demo/io_context_v1.cpp" rel="nofollow ugc">完整代码</a></p>
]]></description><link>http://forum.d2learn.org/topic/189/基于-io_uring-的-c-20-协程网络库-01-基础骨架与-awaiter-机制</link><guid isPermaLink="true">http://forum.d2learn.org/topic/189/基于-io_uring-的-c-20-协程网络库-01-基础骨架与-awaiter-机制</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[启动并分离 - co_spawn]]></title><description><![CDATA[<p dir="auto">在基于 C++20 协程构建的异步框架里，<code>Task&lt;T&gt;</code> 通常有着严格的结构化并发语义：调用者必须去 <code>co_await</code> 它，子任务的生命周期被死死地绑在父任务上。</p>
<p dir="auto">但现实世界没这么理想，系统架构中必然存在<strong>同步世界与异步世界的交汇点</strong>。最典型的例子就是 TCP 服务器的事件循环：</p>
<pre><code class="language-cpp">// 这是一个底层的同步事件循环
void server_accept_loop(io_context&amp; ctx) {
    while (true) {
        stream_socket client = accept_connection(ctx);
        
        // 业务协程：处理单个客户端连接
        // 函数签名：Task&lt;void&gt; handle_client(stream_socket client);
        
        // 问题：怎么在这里启动 handle_client，然后不管它，直接去接下一个客？
        
        // 方案 1: 直接调用？没用，返回值 Task 被丢弃，协程根本不会跑（惰性求值）。
        // handle_client(std::move(client)); 
        
        // 方案 2: 使用 co_await？编译直接报错！因为当前函数是个普通函数，不是协程。
        // co_await handle_client(std::move(client));

        // 方案 3: 把server_accept_loop的返回值改成Task&lt;void&gt;。这里使用co_await
        // 可以编译，但是一次只能处理一个client
    }
}
</code></pre>
<p dir="auto">为了解决这个问题，我们必须提供一个“启动并分离（Fire and Forget）”的方法，类似std::thread的detach模式。这没法用常规的 <code>Task&lt;T&gt;</code> 表达，我们需要自己捏一个底层原语：<code>co_spawn</code>。</p>
<p dir="auto">下面我们将从零开始，一步步打磨出一个内存安全、支持优雅停机的 <code>co_spawn</code>。</p>
<hr />
<h3>1. 裸分离：让编译器帮我们擦屁股</h3>
<p dir="auto">要把一个任务扔到后台，第一个要面对的灵魂拷问就是：<strong>这个协程在堆上分配的内存帧（Coroutine Frame），最后谁来删？</strong></p>
<p dir="auto">既然分离出去了，外部就不存在任何变量持有它的句柄。在 C++20 里，最优雅的解法是：<strong>配置好参数，让编译器自己管理。</strong></p>
<p dir="auto">按照协程规范，只要 <code>promise_type::final_suspend()</code> 返回 <code>std::suspend_never</code>，协程走到生命周期尽头时，运行时就会自动 <code>delete</code> 掉那块堆内存。据此，我们可以写出一个极简的“裸分离”壳子：</p>
<pre><code class="language-cpp">// 纯粹的空壳，仅用于触发编译器的自动清理
struct DetachedTask {
    struct promise_type {
        auto get_return_object() noexcept { return detached{}; }
        
        // 饥饿启动：一创建就立马执行
        auto initial_suspend() noexcept { return std::suspend_never{}; }
        
        // 核心：结束时不挂起，触发自动销毁
        auto final_suspend() noexcept { return std::suspend_never{}; }
        
        void return_void() noexcept {}
        void unhandled_exception() noexcept { std::terminate(); }
    };
};

// 极简版原语
template&lt;typename Awaitable&gt;
auto co_spawn(Awaitable awaitable) -&gt; DetachedTask 
{
    co_await std::move(awaitable);
}
</code></pre>
<p dir="auto">然后，我们在同步回调里调用 <code>co_spawn(handle_client(std::move(client)));</code> 时，协程帧会立即投入运行。遇到 I/O 挂起时，控制流会 <code>return</code> 回主循环（不会阻塞线程！）。等任务彻底跑完，走向 <code>final_suspend</code>，内存安全摧毁，干干净净。</p>
<h3>2. 状态追踪：用 RAII 告别“幽灵任务”</h3>
<p dir="auto">上面这套“裸分离”虽然在语言机制上跑得通，但在工程上其实是个定时炸弹。因为它<strong>没法做状态追踪，也就没法支持服务器的优雅停机（Graceful Shutdown）</strong>。</p>
<p dir="auto">试想一下，如果你发个 <code>SIGTERM</code> 准备关进程，底层的事件分发器怎么知道还有多少个 <code>co_spawn</code> 出去的任务在挂起等 I/O？如果直接把底层上下文销毁了，等这些“幽灵任务”被唤醒时，面对的就是一片废墟，当场 Core Dump。</p>
<p dir="auto">所以，分离出的协程必须和底层的上下文绑定生命周期：诞生时登记，死亡时注销。<br />
这里我们假定一个 <code>context</code> 应该支持 <code>add_work()</code> 和 <code>drop_work()</code> 来管理分离出去的任务。</p>
<pre><code class="language-cpp">// 假定上下文支持增减引用计数
template&lt;typename T&gt;
concept tracking_context = requires(T&amp; ctx) 
{
    ctx.add_work();
    ctx.drop_work();
};

template&lt;tracking_context Context&gt;
struct DetachedTask {
    struct promise_type {
        Context* context = nullptr;

        // 拦截参数：拿到上下文引用，生命周期开始时登记
        template&lt;typename Awaitable&gt;
        promise_type(Context&amp; ctx, Awaitable&amp;&amp;) 
          : context{ &amp;ctx } 
        {
            context-&gt;add_work(); 
        }

        // 绑定析构：随协程帧被编译器销毁时，自动注销
        ~promise_type() 
        {
            if (context) context-&gt;drop_work(); 
        }

        auto get_return_object() noexcept { return DetachedTask{}; }
        auto initial_suspend() noexcept { return std::suspend_never{}; }
        auto final_suspend() noexcept { return std::suspend_never{}; }
        void return_void() noexcept {}
        void unhandled_exception() noexcept { std::terminate(); }
    };
};
</code></pre>
<h3>3. 最终的 <code>co_spawn</code></h3>
<p dir="auto">有了上面这个支持状态追踪的 <code>detached_task</code>，我们就可以给出 <code>co_spawn</code> 的最终接口了。</p>
<pre><code class="language-cpp">// 注意这里的 Awaitable awaitable 是按值传递！
template&lt;tracking_context Context, awaitable Awaitable&gt;
    requires std::movable&lt;std::remove_cvref_t&lt;Awaitable&gt;&gt;
auto co_spawn(Context&amp; ctx, Awaitable awaitable) -&gt; DetachedTask&lt;Context&gt; 
{
    // 移动进协程帧里，生命周期交给DetachedTask
    co_await std::move(awaitable);
}
</code></pre>
<p dir="auto"><strong>为什么要Awaitble必须按值传？</strong><br />
在协程里，如果参数是引用，堆上的协程帧就只会存个指针。像 <code>handle_client(std::move(client))</code> 这种调用，产生的是个临时对象（右值）。如果 <code>co_spawn</code> 接的是个引用，等它内部第一次 <code>co_await</code> 挂起、把控制权还给外层时，这个临时对象早就析构了！这会导致极其隐蔽的悬垂引用Bug。</p>
<p dir="auto">通过强制按值传递，我们用移动语义，把临时的业务任务移动到了协程帧内部，只要协程不死，它的状态就绝对安全。</p>
<h3>4. 补充concept：到底什么是 <code>awaitable</code>？</h3>
<p dir="auto">细心的朋友肯定注意到了，在最终的 <code>co_spawn</code> 签名里，我用了一个 <code>awaitable</code> 的概念。</p>
<p dir="auto">在 C++20 里，一个东西能被 <code>await</code>，无非三种情况：</p>
<ol>
<li>它自己就是个 <code>awaiter</code>，即有3个await函数</li>
<li>它重载了成员方法 <code>operator co_await()</code></li>
<li>有对应的全局重载。</li>
</ol>
<p dir="auto">我们就把这个concept用代码翻译出来：</p>
<pre><code class="language-cpp">template&lt;typename T&gt;
concept awaiter = requires(T&amp; t, std::coroutine_handle&lt;&gt; handle)
{
    { t.await_ready() } -&gt; std::convertible_to&lt;bool&gt;;
    t.await_suspend(handle);
    t.await_resume();
};

template&lt;typename T&gt;
concept has_operator_co_await = requires(T&amp;&amp; t)
{
    { std::forward&lt;T&gt;(t).operator co_await() } -&gt; awaiter;
};

template&lt;typename T&gt;
concept has_global_operator_co_await = requires(T&amp;&amp; t)
{
    { operator co_await(std::forward&lt;T&gt;(t)) } -&gt; awaiter;
};

template&lt;typename T&gt;
concept awaitable = awaiter&lt;T&gt; 
                 || has_operator_co_await&lt;T&gt; 
                 || has_global_operator_co_await&lt;T&gt;;
</code></pre>
<hr />
<h3>实战演示</h3>
<p dir="auto">最后，给一段伪代码示例，看看它是怎么在业务里落地的：</p>
<pre><code class="language-cpp">import std;

// 1. 实现一个满足 tracking_context 契约的上下文
struct MyIOContext {
    int active_tasks = 0;
    
    void add_work() 
    {
        ++active_tasks;
        std::cout &lt;&lt; "[Context] 任务+1，当前活跃数: " &lt;&lt; active_tasks &lt;&lt; "\n";
    }
    
    void drop_work() 
    {
        --active_tasks;
        std::cout &lt;&lt; "[Context] 任务结束，当前活跃数: " &lt;&lt; active_tasks &lt;&lt; "\n";
    }
    
    void run_loop() 
    {
        // 真实场景里，这里是 epoll_wait 或 io_uring_enter 阻塞等事件
        std::cout &lt;&lt; "[Context] 开启事件循环，等待 I/O...\n";
    }
};

// 2. 模拟一个能被 co_await 的异步操作 (满足 awaitable 契约)
struct DummyAsyncRead {
    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle&lt;&gt;) 
    {
        std::cout &lt;&lt; "  -&gt; 协程挂起，把 fd 注册到 epoll...\n";
    }

    void await_resume() 
    {
        std::cout &lt;&lt; "  -&gt; 协程恢复，拿到数据！\n";
    }
};

// 业务逻辑协程
DummyAsyncRead handle_client(int client_fd) 
{
    std::cout &lt;&lt; "开始处理客户端: " &lt;&lt; client_fd &lt;&lt; "\n";
    // 遇到 IO 挂起
    co_await DummyAsyncRead{}; 
}

// 3. 跑起来
int main() {
    MyIOContext ctx;
    
    std::cout &lt;&lt; "--- 服务器启动 ---\n";
    
    // 启动并分离，立刻返回
    co_spawn(ctx, handle_client(1001));
    co_spawn(ctx, handle_client(1002));
    
    std::cout &lt;&lt; "--- 同步的 main 函数丝毫不受阻塞 ---\n";
    
    ctx.run_loop();
    
    return 0;
}
</code></pre>
]]></description><link>http://forum.d2learn.org/topic/188/启动并分离-co_spawn</link><guid isPermaLink="true">http://forum.d2learn.org/topic/188/启动并分离-co_spawn</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item><item><title><![CDATA[从零构建基于 C++20 的 Task]]></title><description><![CDATA[<p dir="auto">C++20 引入了无栈协程（Stackless Coroutines）的核心语言机制，但与之相配套的标准库高级抽象（如 <code>std::task</code>）并未同步提供。在构建基于 <code>io_uring</code> 或 <code>epoll</code> 的高性能并发框架时，我们不可避免地需要自行设计一个用于封装异步操作的返回类型：<code>Task&lt;T&gt;</code>。</p>
<p dir="auto">设计这样一个任务类型，不仅仅是对新关键字的语法包装，其本质是在解决两个系统级编程的核心问题：</p>
<ol>
<li><strong>控制流的无缝路由</strong></li>
<li><strong>堆分配状态帧的确定性释放</strong>。</li>
</ol>
<p dir="auto">本文将探讨如何从零构建一个可用的Task&lt;T&gt;。</p>
<h2>1. 异步组合的困境与懒启动（Lazy Evaluation）</h2>
<p dir="auto">在传统的同步流中，函数的调用即意味着执行的开始。但在异步架构中，任务的“构造”与“执行”往往需要被严格分离。</p>
<p dir="auto">为了建立直观的理解，我们可以先参考 Python 中的协程行为。在 Python 中，调用一个 <code>async def</code> 函数并不会立即执行其内部代码，而是仅仅返回一个协程对象：</p>
<pre><code class="language-python">import asyncio

async def fetch_data():
    print("开始发起网络请求...")
    # ...

# 此时并不会打印任何内容，仅仅是构造了一个任务对象
task = fetch_data() 

# 只有显式地等待或交给事件循环，代码才会真正运转
# await task 
</code></pre>
<p dir="auto">这种机制被称为<strong>懒启动（Lazy Evaluation）</strong>。如果我们允许 C++ 的协程在被调用时立即开始执行（即所谓的 Eager Evaluation），它可能会在尚未正确挂载到事件循环（Event Loop）之前，就过早地触发了底层的 I/O 投递操作。这不仅破坏了状态的封装，还极易引发复杂的竞态条件。</p>
<p dir="auto">因此，一个健壮的 C++ <code>Task&lt;T&gt;</code> 必须是懒启动的。这在 C++20 中是通过定制 <code>promise_type</code> 的初始化行为来实现的：</p>
<pre><code class="language-cpp">class promise_type {
public:
    // 协程帧创建后立即挂起，不主动执行协程体代码
    auto initial_suspend() noexcept -&gt; std::suspend_always { return {}; }
    // ...
};
</code></pre>
<p dir="auto">通过返回 <code>std::suspend_always</code>，协程在完成内部状态帧的堆分配后会立刻交出控制权。这种设计使得异步任务可以像普通的数据结构一样被安全地传递、存储和组合，直到调用者显式地通过 <code>co_await</code> 来驱动它。</p>
<h2>2. 协程间的控制流移交</h2>
<p dir="auto">异步操作很少是孤立存在的。当父协程执行 <code>co_await child_task;</code> 时，当前的执行流必须被挂起，并将 CPU 的控制权移交给子协程。同时，子协程必须知晓在自身执行完毕后，应该唤醒哪一个调用者。</p>
<p dir="auto">为了建立这种调用链，我们利用了 <code>co_await</code> 运算符所触发的编译器协议。</p>
<p dir="auto">在 C++20 中，<code>co_await</code> 并非一个简单的挂起指令，而是一个<strong>可定制的控制流拦截点</strong>。当编译器遇到 <code>co_await &lt;expr&gt;</code> 时，它会要求 <code>&lt;expr&gt;</code> 产出一个符合特定接口的 <code>Awaiter</code> 对象，并依次调用其三个核心方法：</p>
<ol>
<li><strong><code>await_ready()</code></strong>：探测状态。询问异步操作是否已经完成。如果返回 <code>true</code>，编译器将走“快速通道”，直接跳过挂起阶段；如果返回 <code>false</code>，则准备挂起当前协程。</li>
<li><strong><code>await_suspend(std::coroutine_handle&lt;&gt;)</code></strong>：核心拦截点。在当前协程的物理状态（寄存器、局部变量）被安全保存到堆上的协程帧后，编译器会调用此方法，并将当前（父）协程的句柄作为参数传入。</li>
<li><strong><code>await_resume()</code></strong>：结果提取点。当协程被再次唤醒时，此方法的返回值将作为整个 <code>co_await</code> 表达式的结果。</li>
</ol>
<p dir="auto">基于这一协议，我们在 <code>Task</code> 内部定义了专门的 <code>Awaiter</code>，以此来接管并路由控制流：</p>
<pre><code class="language-cpp">class Awaiter {
public:
    explicit Awaiter(handle_type handle) : handle_{ handle } {}

    // 1. 探测状态：如果子协程尚未执行完毕，则强制父协程挂起
    bool await_ready() const noexcept 
    { 
        return !handle_ || handle_.done(); 
    }

    // 2. 挂起时的控制流路由
    auto await_suspend(std::coroutine_handle&lt;&gt; next) -&gt; std::coroutine_handle&lt;&gt; 
    {
        // 将父协程的句柄 (next) 记录在子协程的 promise 状态中
        handle_.promise().next = next;
        // 返回子协程的句柄，指示 C++ 运行时将执行流切换至子协程
        return handle_;
    }

    // 3. 唤醒后的结果提取
    auto await_resume() const -&gt; T 
    {
        if (!handle_) throw std::logic_error{ "Invalid handle" };
        return handle_.promise().result();
    }

private:
    handle_type handle_; // 子协程的句柄
};
</code></pre>
<p dir="auto">通过这一套状态机转换，C++ 将底层的调度权完全下放给了库作者。</p>
<p dir="auto">在 <code>await_suspend</code> 执行的瞬间，父协程已被安全冻结。</p>
<p dir="auto">我们将其句柄保存在子协程的 <code>promise_type::next</code> 字段里，从而在内存中建立了一个单向的调用链表（父 -&gt; 子）。</p>
<p dir="auto">紧接着返回子协程的 <code>handle_</code>，运行时会直接跳转执行子协程代码，实现了零开销的上下文切换。</p>
<h2>3. 栈溢出风险与对称传输（Symmetric Transfer）</h2>
<p dir="auto">子协程执行到末尾（或遇到 <code>co_return</code>）时，需要唤醒之前等待它的父协程。这往往是自定义协程实现中最容易出错的环节。</p>
<p dir="auto">直觉上的做法是，在子协程的收尾阶段直接调用 <code>next.resume()</code>。然而，这种非对称传输（Asymmetric Transfer）存在致命缺陷：</p>
<p dir="auto"><code>resume()</code> 本质上是一个常规的同步函数调用。</p>
<p dir="auto">在网络服务这类存在深层嵌套或无限循环挂起的场景中（例如 <code>while(true) { co_await read(); }</code>），每一次 <code>resume()</code> 都会在操作系统的线程栈上压入一个新的栈帧。调用链越长，栈越深，最终必然导致 Stack Overflow（栈溢出）。</p>
<p dir="auto">为了提供工业级的稳定性，<code>Task</code> 在收尾时必须采用<strong>对称传输（Symmetric Transfer）</strong>：</p>
<pre><code class="language-cpp">class FinalAwaiter {
public:
    bool await_ready() const noexcept { return false; }

    template&lt;typename Promise&gt;
    auto await_suspend(std::coroutine_handle&lt;Promise&gt; handle) const noexcept -&gt; std::coroutine_handle&lt;&gt; 
    {
        auto next = handle.promise().next;
        // 关键点：直接返回父协程的句柄，而非调用 next.resume()
        return next ? next : std::noop_coroutine();
    }

    void await_resume() const noexcept {}
};

// 在 promise_type 中指定收尾行为：
auto final_suspend() noexcept -&gt; FinalAwaiter { return {}; }
</code></pre>
<p dir="auto">通过让 <code>final_suspend</code> 返回一个包含父协程句柄的 <code>Awaiter</code>，编译器会采用类似尾调用优化（Tail Call）的机制：</p>
<p dir="auto"><strong>它会首先将当前子协程的物理栈帧安全剥离，然后再以平级跳转的方式进入父协程。</strong></p>
<p dir="auto">在这种机制的保障下，无论业务逻辑中 <code>co_await</code> 嵌套了多少层，底层的线程调用栈深度始终保持恒定 (O(1))。</p>
<h2>4. 返回值的提取与异常路由</h2>
<p dir="auto">异步任务不仅涉及控制流的跳转，还必须安全地跨越挂起边界传递数据或异常，并且表现得如同普通的 C++ 函数调用一样。</p>
<p dir="auto">在子协程内部，产生的值或未捕获的异常被分别存储在 <code>promise_type</code> 的 <code>std::optional&lt;T&gt;</code> 和 <code>std::exception_ptr</code> 中。当父协程通过对称传输被唤醒，并执行 <code>await_resume()</code> 时，需要提取这些结果：</p>
<pre><code class="language-cpp">auto result() -&gt; T 
{
    if (exception_) 
        std::rethrow_exception(exception_);
    return std::move(value_).value();
}
</code></pre>
<p dir="auto">这里包含两个重要的设计约束：</p>
<ol>
<li><strong>异常透明性</strong>：<code>std::rethrow_exception</code> 确保了子协程中发生的异常能够被无缝抛出，并被父协程的 <code>try-catch</code> 块捕获，维持了 C++ 异常处理语义的连贯性。</li>
<li><strong>资源所有权转移</strong>：通过 <code>std::move</code> 提取值，保证了诸如 <code>std::unique_ptr</code> 或封装了系统资源（如文件描述符）的不可拷贝对象（Move-Only Types）能够被正确返回。</li>
</ol>
<h2>5. 协程帧的生命周期管理与单次消费语义</h2>
<p dir="auto">无栈协程的局部变量和 <code>promise_type</code> 被编译器分配在堆上的协程帧（Coroutine Frame）中。由于 C++ 没有垃圾回收机制，资源泄漏是协程编程中的主要风险之一。</p>
<p dir="auto">依据 C++ 核心的 RAII（资源获取即初始化）原则，<code>Task</code> 对象作为协程句柄的唯一持有者，理应负责这块内存的清理：</p>
<pre><code class="language-cpp">template&lt;typename T&gt;
class Task {
public:
    ~Task() 
    { 
        if (handle_) handle_.destroy(); 
    }

    // 限制为右值调用，且不转移 handle_ 的所有权
    auto operator co_await() &amp;&amp; noexcept { return Awaiter{ handle_ }; }
};
</code></pre>
<p dir="auto">这里有两个深思熟虑的设计权衡：</p>
<p dir="auto"><strong>第一：为什么限制 <code>operator co_await</code> 为右值版本（<code>&amp;&amp;</code>）？</strong><br />
协程代表一个异步计算过程，其内部结果（特别是前文提到的 Move-Only 类型）在 <code>await_resume</code> 中是被破坏性提取的（<code>std::move</code>）。这意味着一个 <code>Task</code> 在逻辑上只能被消费一次。如果允许对左值的 <code>Task</code> 进行 <code>co_await</code>，调用者可能会意外地多次等待同一个任务：</p>
<pre><code class="language-cpp">Task&lt;int&gt; t = do_work();
auto res1 = co_await t;
auto res2 = co_await t; // 错误：底层协程已经结束，状态帧已被销毁
</code></pre>
<p dir="auto">通过添加 <code>&amp;&amp;</code> 限定符，我们利用 C++ 的类型系统在编译期强制执行了“单次消费（Single-Shot）”语义。调用者必须直接等待临时对象（如 <code>co_await do_work();</code>），或者显式地转移所有权（<code>co_await std::move(t);</code>）。这在接口层面明确了状态机的生命周期契约。</p>
<p dir="auto"><strong>第二：为什么在右值版本中，依然不剥夺 Task 的所有权？</strong><br />
通常在处理右值时，我们会使用 <code>std::exchange</code> 来转移底层资源。但在这里，我们仅向 <code>Awaiter</code> 传递了句柄的值。<br />
当执行 <code>auto res = co_await do_work();</code> 时，<code>do_work()</code> 产生的 <code>Task</code> 临时对象的生命周期会被编译器自动延续，直到整个 <code>co_await</code> 表达式求值完毕（即 <code>await_resume()</code> 返回之后）。此时，临时 <code>Task</code> 对象被析构，从而触发 <code>handle_.destroy()</code>。<br />
如果我们在此处剥夺了 <code>Task</code> 的所有权，清理责任就会落空。这种保留所有权的设计，确保了无论是正常执行完毕还是因异常提前中断，底层堆内存都能依托 <code>Task</code> 临时对象的析构函数被可靠地回收，实现了严格的内存安全。</p>
<p dir="auto"><strong>补充说明</strong><br />
在 C++ 中，临时对象的生命周期会持续到包含它的完整表达式（Full-expression）结束（通常是遇到分号 ;）。</p>
<p dir="auto">当我们写下如下代码时：</p>
<pre><code class="language-c++">auto res = co_await do_something();
</code></pre>
<p dir="auto">编译器实际上会做如下展开（伪代码）：</p>
<pre><code class="language-c++">{
    // 1. 调用函数，产生临时的 Task 右值对象
    auto&amp;&amp; __tmp_task = do_something(); 
    
    // 2. 调用 operator co_await，产生临时的 Awaiter 对象
    auto&amp;&amp; __awaiter = __tmp_task.operator co_await();
    
    if (!__awaiter.await_ready()) {
        // 3. 挂起当前协程，并调用 await_suspend
        __awaiter.await_suspend(current_coro_handle);
        // &lt;--- 协程在这里彻底挂起，CPU 离开 ---&gt;
        // &lt;--- 时空流转，无论过了多久，终于被唤醒 ---&gt;
    }
    
    // 4. 唤醒后，调用 await_resume 提取结果
    auto res = __awaiter.await_resume();
    
} // 5. 完整表达式结束！按照构造的相反顺序销毁临时对象：先销毁 __awaiter，再销毁 __tmp_task
</code></pre>
<p dir="auto">关键点在于： 协程在挂起时，编译器非常清楚 __tmp_task 和 __awaiter 的生命周期需要跨越挂起点。因此，编译器不会把它们分配在容易被销毁的线程栈（Thread Stack）上，而是直接将它们作为局部变量，打包存储在“当前（父）协程的堆分配状态帧（Coroutine Frame）”中。</p>
<p dir="auto">这意味着：</p>
<p dir="auto">Task 对象在整个挂起期间一直安然无恙地活在堆内存里。</p>
<p dir="auto">唤醒时，Awaiter 也并没有在栈上重建，你访问的依然是挂起前保存在堆里的那个确切的 Awaiter 实例。</p>
<p dir="auto">Task 必定比 Awaiter 活得更久（先构造的后销毁）。</p>
<p dir="auto">因此，Awaiter 内部仅持有 handle_ 的一个浅拷贝是绝对安全的，Task 完全不需要把所有权 exchange 给 Awaiter。</p>
<h2>结语</h2>
<p dir="auto">设计一个现代 C++ 的 <code>Task</code> 类，并非对关键字的简单拼接，而是对执行流跳转和资源生命周期的精密编排。通过懒启动隔离控制流、利用对称传输突破调用栈限制、借助 RAII 保障内存释放，我们最终构建出了一个符合 C++ 哲学体系的高性能并发原语。</p>
<h3>完整代码</h3>
<pre><code class="language-c++">export module xin.task;

import std;

namespace xin {

class FinalAwaiter {
public:
    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    constexpr auto await_ready() const noexcept -&gt; bool
    {
        return false;
    }

    template&lt;typename Promise&gt;
    auto await_suspend(std::coroutine_handle&lt;Promise&gt; handle) const noexcept -&gt; std::coroutine_handle&lt;&gt;
    {
        auto next = handle.promise().next;
        return next ? next : std::noop_coroutine();
    }

    void await_resume() const noexcept {}
};


export template&lt;typename T = void&gt;
class Task;

export template&lt;typename T&gt;
class Task {
public:
    class promise_type;
    using handle_type = std::coroutine_handle&lt;promise_type&gt;;

    class promise_type {
    public:
        auto get_return_object() noexcept -&gt; Task { return Task{ handle_type::from_promise(*this) }; }

        auto initial_suspend() noexcept -&gt; std::suspend_always { return {}; }

        auto final_suspend() noexcept -&gt; FinalAwaiter { return {}; }

        void unhandled_exception() noexcept { exception_ = std::current_exception(); }

        template&lt;typename U&gt;
            requires std::convertible_to&lt;U&amp;&amp;, T&gt;
        void return_value(U&amp;&amp; value) noexcept(std::is_nothrow_constructible_v&lt;T, U&amp;&amp;&gt;)
        {
            value_.emplace(std::forward&lt;U&gt;(value));
        }

        &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
        auto result() -&gt; T
        {
            if (exception_)
                std::rethrow_exception(exception_);

            if (!value_)
                throw std::logic_error{ "No value returned from coroutine" };

            auto out = std::move(*value_);
            value_.reset();
            return out;
        }

        std::coroutine_handle&lt;&gt; next{ nullptr };

    private:
        std::exception_ptr exception_;
        std::optional&lt;T&gt; value_;
    };

    Task() = default;

    Task(handle_type handle)
      : handle_{ handle }
    {}

    Task(const Task&amp;) = delete;
    auto operator=(const Task&amp;) -&gt; Task&amp; = delete;

    Task(Task&amp;&amp; other) noexcept
      : handle_{ std::exchange(other.handle_, {}) }
    {}

    auto operator=(Task&amp;&amp; other) noexcept -&gt; Task&amp;
    {
        if (this == &amp;other)
            return *this;

        if (handle_)
            handle_.destroy();

        handle_ = std::exchange(other.handle_, nullptr);
        return *this;
    }

    ~Task()
    {
        if (handle_)
            handle_.destroy();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto done() const noexcept -&gt; bool
    {
        return !handle_ || handle_.done();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto handle() const noexcept -&gt; handle_type
    {
        return handle_;
    }

    class Awaiter {
    public:
        explicit Awaiter(handle_type handle)
          : handle_{ handle }
        {}

        &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
        auto await_ready() const noexcept -&gt; bool
        {
            return !handle_ || handle_.done();
        }

        auto await_suspend(std::coroutine_handle&lt;&gt; next) -&gt; std::coroutine_handle&lt;&gt;
        {
            handle_.promise().next = next;
            return handle_;
        }

        auto await_resume() const -&gt; T
        {
            if (!handle_)
                throw std::logic_error{ "Invalid coroutine handle" };

            return handle_.promise().result();
        }

    private:
        handle_type handle_;
    };

    auto operator co_await() &amp;&amp; noexcept { return Awaiter{ handle_ }; }

private:
    handle_type handle_{ nullptr };
};


export template&lt;&gt;
class Task&lt;void&gt; {
public:
    class promise_type;
    using handle_type = std::coroutine_handle&lt;promise_type&gt;;

    class promise_type {
    public:
        std::coroutine_handle&lt;&gt; next{ nullptr };

        auto get_return_object() noexcept -&gt; Task { return Task{ handle_type::from_promise(*this) }; }

        auto initial_suspend() noexcept -&gt; std::suspend_always { return {}; }

        auto final_suspend() noexcept -&gt; FinalAwaiter { return {}; }

        void unhandled_exception() noexcept { exception_ = std::current_exception(); }

        void return_void() noexcept {}

        void result()
        {
            if (exception_)
                std::rethrow_exception(exception_);
        }

    private:
        std::exception_ptr exception_;
    };

    Task() = default;

    Task(handle_type handle)
      : handle_{ handle }
    {}

    Task(const Task&amp;) = delete;
    auto operator=(const Task&amp;) -&gt; Task&amp; = delete;

    Task(Task&amp;&amp; other) noexcept
      : handle_{ std::exchange(other.handle_, nullptr) }
    {}

    auto operator=(Task&amp;&amp; other) noexcept -&gt; Task&amp;
    {
        if (this == &amp;other)
            return *this;

        if (handle_)
            handle_.destroy();

        handle_ = std::exchange(other.handle_, {});
        return *this;
    }

    ~Task()
    {
        if (handle_)
            handle_.destroy();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto done() const noexcept -&gt; bool
    {
        return !handle_ || handle_.done();
    }

    &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
    auto handle() const noexcept -&gt; handle_type
    {
        return handle_;
    }

    class Awaiter {
    public:
        explicit Awaiter(handle_type handle)
          : handle_{ handle }
        {
        }

        &lsqb;&lsqb;nodiscard&rsqb;&rsqb;
        auto await_ready() const noexcept -&gt; bool
        {
            return !handle_ || handle_.done();
        }

        auto await_suspend(std::coroutine_handle&lt;&gt; next) -&gt; std::coroutine_handle&lt;&gt;
        {
            handle_.promise().next = next;
            return handle_;
        }

        void await_resume() const
        {
            if (!handle_)
                throw std::logic_error{ "Invalid coroutine handle" };

            handle_.promise().result();
        }

    private:
        handle_type handle_;
    };

    auto operator co_await() &amp;&amp; noexcept { return Awaiter{ handle_ }; }

private:
    handle_type handle_{ nullptr };
};

} // namespace xin
</code></pre>
]]></description><link>http://forum.d2learn.org/topic/187/从零构建基于-c-20-的-task</link><guid isPermaLink="true">http://forum.d2learn.org/topic/187/从零构建基于-c-20-的-task</guid><dc:creator><![CDATA[Doomjustin]]></dc:creator><pubDate>Invalid Date</pubDate></item></channel></rss>