基于协程io_uring 异步网络库系列 IV: Proactor 与 asynchronous operation | C++20 coroutine | io_uring 异步IO 网络框架

本系列通过结合 linux 的 io_uring 和 cppcoro (源码需要进行部分修改以适配 linux 下的 g++-11)在网络中的使用学习 C++20 coroutine。值得注意的是,cppcoro 目前已经暂停维护,仍然为 TS 版本的支持,同时其真异步底层支持只支持了 win32 的 IOCP(本身 cppcoro 兼容 MSVC),但是本系列不想涉及 IOCP 和 windows 的部分因为除了跨平台外,没有太大意义(如果采用 windows 的话,C# 是足够好的语言,但是目前广泛的服务器应用一般采用 linux)。

专栏内的所有笔记本身是和他们自洽的(也许漏了一篇讲如何理解协程和函数式编程中的 call/cc 的笔记,博客中也上传了,当然实际这系列笔记不是一个能够快速上手的,而是一个系列的学习,主要目的是供我自己复习或者有对 C++ 协程与 Proactor 网络框架编写感兴趣的读者。

本系列本身是内容自洽的如果前提得到了满足即读者(我)必须具备了 C++20 coroutine 的状态机(当然,通用的状态机本身也可以用协程实现,不过这已经离题了,可以另外开笔记来讲怎么做)基础流程在脑子里以及 io_uring 的 liburing API(不是必须的),而这些内容都可以在上面附的文章中得到答案(以及较官方的资料,如 io_uring 的除了本身的 pdf 、邮件列表、还有专门的网站)。尽管这里废话会很多,虽然废话的存在本身可能是因为要用来构造一种常识/直觉,而省去了会简洁很多但是要求读者分布式去中心化地具有这些常识,实际学到的东西很少,没有任何的挑战,而且必须是烂尾预定的系列,但是我还没学会高效的记录方式,很容易忘掉细节。本身看这些也没有用,可能搞懂现有的基础设施的源码对调优还有一点意义。我备注一下实际如果要读源码的话,需要准备 boost asio 支持 coroutine ts 的版本(主要看 example)、cppcoro 的代码库(以及 readme 的示例)。

cppcoro 源码级使用教程系列: 概述 | C++20 coroutine 教程 | io_uring 异步IO 网络框架 系列笔记_我说我谁呢 --CSDN博客


好,现在我们就来讲怎么写最底层的真异步的封装。这个需求会出现在,你要读文件、读 socket 读网络远程等等之上做的事情。但是他是不是可以完全让像 cppcoro 这种库来提供呢?理论上是的,像 socket 、file 的读写,是不是可以统一写好呢?然而这次我们就是要和 os 接口 io_uring 打交道,而且 cppcoro 就是没有被 io_uring 赶上。当然还有什么需求,我们知道像一些 compute-bound 的比如你压缩解压缩那些,可能是要用到线程池(这个是下一篇的内容),但是比如你做一个 mysql connector 就可能要写这种 awaiter 了。可能还有一个疑问,比如我只需要构建好 mysql 的请求数据包不就行了吗,这个过程是同步的,然后再调用异步的 co_await 最底层的比如发 socket ,对的,就是这种层次结构,C# 的异步也是这样层次建立起来的。这意味着你只需要写 task 就行了!是不是很爽!这就是 task 的重要性了,他是一个套娃中介,是递归递归的非初始条件。那么结论其实是,除非你要添加 OS 的需求,不然 task 已经够你构建上层的异步封装了(如果有无请指出,当然本来博客也没别人看的,我就自己什么时候想到别的再来修改了)。不过,我们现在就是在写 io_uring 的封装,所以了解一下还是有点用的,说不定哪天要支持另一个 jo_uring ko_uring 了。


按照惯例,我们还是再复习一下这个 co_await 的状态机怎么跑的:

	// https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
	{
	  auto&& value = <expr>;
	  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
	  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
	  if (!awaiter.await_ready()){
	    using handle_t = std::experimental::coroutine_handle<P>;
	    using await_suspend_result_t =
	      decltype(awaiter.await_suspend(handle_t::from_promise(p)));
	    <suspend-coroutine>
	    if constexpr (std::is_void_v<await_suspend_result_t>){
	      awaiter.await_suspend(handle_t::from_promise(p));
	      <return-to-caller-or-resumer>
	    }
	    else{
	      static_assert(
	         std::is_same_v<await_suspend_result_t, bool>,
	         "await_suspend() must return 'void' or 'bool'.");
	      if (awaiter.await_suspend(handle_t::from_promise(p))){
	        <return-to-caller-or-resumer>
	      }
	    }
	    <resume-point>
	  }
	  return awaiter.await_resume();
}

一直没有复习协程的 R 以及 R::promise_type,其实他们本来就不重要,主要是提供 promise_type 作为堆上存储,然后为 transform 提供了支持、为一开始 initial_suspend \final_suspend 提供了 decorator 的 hook 支持、以及完成 co_yield、co_return 逻辑而已。不过一个事情是,我们到现在都没有用上 co_yield 和 co_return,(其实 yield 在 cppcoro 基本组件里面见过,但是没分析过),不过你已经明白了,我们的 task 只写了一半(在文章 C++20 coroutine 探索III: 异步编程,Task<T> 编写,boost asio 协程分析,C# async / await, cppcoro 源码分析 中),但是我说,时间关系暂时留白,结果就烂尾了。实际我们当然可以


这里我直接给出结论吧,首先因为 await_suspend 的签名返回值可以是 bool,如果不需要 suspend,就会马上 resume,意味着你可以支持第一次尝试能不能直接就以同步方式运行结束。然后是 coroutine_handle 要被封装丢进操作系统提供的 comletion token 机位(无论是 IOCP 的还是 io_uring 都提供了)里等到被恢复。await_suspend 里面要完成的事情就是直接投递异步请求给 OS。本段完毕。至于你的 io_context (iouring 的 fd) 从哪里来这个看怎么设计接口了,比如 asio 必须先 co_await 去捕获一个(本质上是要下层实现一个线程管理器)executor,还有一种做法是 acceptor 本身就要通过参数传一个 executor 进来(如果你不封装 executor,并且一个 io_service 一把梭一个单线程,那么可以直接传 io_service 进去),还有一种做法就是,暴力全局变量就行了,提供一个 thread_local,get_thread_executor() 类似的方法。


然后 cppcoro 也是特别靠谱的采用了 windows 的方案(因为那个时候还没有 io_uring, 我猜想作者会把 io_uring 支持加上如果两个技术的时间上重合),我们看到的是,由于 IOCP 本身就支持 callback,所以 cppcoro 直接给注册了一个 callback 给  windows ,期待完成之后,windows 将会帮我们调用 coroutine_handle  的 resume,实际上 windows 自己会准备一些核心数线程准备运行这些工作,值得注意的是 windows 上的线程十分的高级所以开销可能是不大的,十分感动。但是到 io_uring 就不是这样的了,我们需要编写我们自己的 proactor,事情完成后,我们需要提取 coroutine_handle 然后进行 resume。

当然我前面说了,我们并不一定需要多线程嘛。单线程也是可以的!也就是开一个 main,然后他一开始就 async void 投一个 accept 的请求,然后 acceptor 的 coroutine_handle 被抓走了, main free 了,他就可以执行一个死循环睡觉在 io_uring_wait_cqe (饿,具体名字其实我忘记了,我乱写的),当然他已经投递了 eventfd(话说我忘记是这一篇还是前面的篇目讲的了),然后一旦有一个完成了,就优先响应这个,然后我们提取 handle 出来 resume,由于 resume 的过程应该是很快的(而且这次不需要包装 async void 了)。


然后补充一个问题吧,就是说 accept 本身不一定会阻塞,io_uring 对待这些可能不会阻塞的 API 是怎么优化的呢?(因为 cppcoro 里面的这个 suspend 前的检测不会阻塞是通过 try 一 try 知道的,顺便补充一个点,就是说 await_ready 和 await_suspend 都可以判断是否 ready ,但是 suspend 多了一个打包 continuation 的过程,开销更大。通过 ready 先非阻塞询问一次可能是一个好方案,然而 kernel developer 应该很聪明,这样凭空增加 system call 的事情我认为是不会被做出来的(存疑,实际编程禁止主观臆测),所以你直接 submit 吧,如果可以的话直接回来就行了,不用用到 await_ready 来做 shortcut)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值