欢迎光临散文网 会员登陆 & 注册

boost asio 的一些问题

2023-02-23 16:19 作者:Meriex  | 我要投稿


周末用 boost asio 配合 beast::http 写了一个小的异步 https 客户端 demo,可以通过 cookie 从 bilibili 服务器获取本机已登陆用户的 id 还有所佩戴的徽章(只拿了一些简单的信息,比如徽章名字还有等级),我自己的构思中这可以作为一些粉丝服务的前置信息。(比如大家都是 Asaki 的粉丝然后就可以直接转到一个聊天室这种)


那在实现的过程中就发现不可避免的碰到了回调地狱的问题(因为自己对协程其实了解不多所以也没有用相关特性),这里做一个小的总结:


首先我们正常或者说大部分时间写代码都是以一种同步的方式进行的,比如我现在写 cephfs 的代码, client 要给 mds 发送一条消息请求 open 一个文件,这个流程是怎么样的,他一定是三步走:

  1. make_request 创建一条 open 请求

  2. 向 mds 发送这个请求并等待 mds 处理

  3. 处理 mds 回复并向 kernel 返回结果

在这个过程中我们说只要 mds 没有处理完成这个 open 请求,那么 client 或者说 client 的这个线程就一定会阻塞在 wait() 处。什么,你想处理多个请求?那么请使用多个线程来处理这些请求,同样的,每个线程都会阻塞在各自的 wait() 处等待 mds 返回。


使用多线程技术确实可以减少 cpu 算力出现空置的情况,但紧接着 c10k 问题的普及让人们发现 cpu 对大量线程的调度实际上是对 cpu 性能的致命浪费,这严重影响了服务器的绝对性能,因此以 nginx 为代表的异步服务器设计出现并成为主流,同时异步编程方式也为大家所熟知。


如果我们使用异步的方式去改写刚才 open 文件的过程,就会像下面这样:

  1. make_request 创建一条 open 请求

  2. 向事件队列提交一个异步操作(向 mds 发送这个请求)并立即返回

  3. make_request 创建第二条请求

  4. 向事件队列提交一个异步操作并立即返回

  5. 启动事件循环向 mds 发送请求

  6. mds 返回第一个请求, client 通过提前设置的回调处理并返回

  7. mds 返回第二个请求, client 通过提前设置的回调处理并返回

以上的所有过程都是在同个线程中完成,因此避免了线程切换,提高了 cpu 利用率,唯一的阻塞点在启动事件循环(因为需要等待返回)


在 asio 中,提交异步操作就是通过 async_xxx 函数完成,而执行异步操作以及监听事件循环就是通过 io_context.run() 触发


但是这样就不可避免的遇到一个问题,如果一个操作有多个步骤并且多个步骤都依赖上一步完成,那么我们就必须在上一步的回调中执行下一步操作,比如我要和 api.bilibili.com 建立一个 tls 连接:

  1. 提交一个解析域名的异步操作 async_resolve

  2. 设置 async_resolve 的回调函数为提交一个连接到 server 的异步操作 async_connect

  3. 设置 async_connect 的回调函数为提交一个建立 tls 连接的异步操作 async_handshake

  4. 设置 async_handshake 的回调函数将 session 状态设为 ready

  5. 通过 io_context.run() 开始事件循环

  6. async_resolve 返回,通过回调函数调用 async_connect

  7. async_connect 返回,通过回调函数调用 async_handshake

  8. async_handshake 返回,通过回调函数将 session 状态设为 ready


简化的代码大概类似这样:

这种链式的调用方式导致我们很难对代码进行拆分和抽象,这也是“回调地狱”的由来,你的所有操作只能通过一条递归的线来进行,除非通过多线程配合 promise 和 future 还有可能解决,单线程中我感觉无解。


那解决办法的话之前其实也提到了,通过协程来实现,也就是常看到的“以同步方式写异步代码”


所以说协程还是绕不过去的坎啊,哎,学,现在就学。


boost asio 的一些问题的评论 (共 条)

分享到微博请遵守国家法律