asio 网络库

asio 封装了平台差异性,在 windows 下本质就是 IOCP。使用 IOCP 请看这里

ASIO

Your program will have at least one I/O execution context, such as an asio::io_context object, asio::thread_pool object, or asio::system_context. This I/O execution context represents your program’s link to the operating system’s I/O services.

理解 C++ Executor 的设计理念,查看 asio Executor requirements,就只是朴素的 callable thing 的执行器概念

基于 Asio 的 C++ 网络编程

Boost asio 官方教程

Documentation (non-Boost)

查看 asio 源码,可以学到“错误代码 vs 异常”两种策略;可以学到“同步 vs 异步”两种接口形式。

如果依赖 IO 的结果,(单线程)异步 IO 和同步阻塞 IO 都要等到 IO 完成才能继续执行,那么异步的性能是否更好?如果是,为什么?—— 理解 iocp 之后是否就“豁然开朗”了呢

有一点也需要注意, 就是从 request 进来到完成, 应用内各个节点和调用流程都要支持异步 io 调用, 否则一个节点不支持, 就退化成多线程的解决方式。摘自

ps. 如果某个节点特别耗时,阻塞当前线程,其实也就退化了

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程。摘自

在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

要理解 asio,其实就是 windows 下的 iocp,就是要理解 Proactor

如果要在连续下载文件中使用 asio 异步模型,那么如何将下载后 sqlite3 入库改为嵌入 handler 中?

例子学习笔记

2019/8/29 16:07:03 因为多数函数都是模板,编码时信息提示本就是短板。结合 co_await 运算符使用时,更是得不到任何提示信息。只能“瞎写”参数数目和类型,编译报错 + 查看源码多次试验究竟怎么写入参。

daytime_client.cpp

结合 c++11 std::future 使用 asio 的异步接口,只是在局部提升效率:每个异步接口返回的 future,在其他异步接口中使用时要传入 future::get() 结果,**而非 future**。从全局来看依旧是阻塞的。

当然,有些业务场景恰好如此。一方面想改进每次同步 IO 的阻塞,因为两次 IO 之间可以做些其余工作;另一方面,下一次 IO 调用依赖前一次 IO 的结果。如果为了局部改进,整体改用「异步 + 回调」形式,开发耗时久:“回调地狱”真的很累,难写,看的人也累。

在上述场景中,如果没有 两次 IO 之间可以做些其余工作 的需求,其实再次“退化”改用同步接口即可。

协程

Boost ASIO supports 3 coroutine types:

无栈协程 ☆

Boost 库中的协程支持两种方式:

  • 一种是封装了 Boost.Coroutinespawn,是一个 stackful 类型的协程;
  • 一种是 asio 作者写出的 stackless 协程。

asio 的作者通过 Duff’s Device 技术 实现的无栈协程,用到了好多奇技淫巧。结合 coroutine 类 的手册和 官方 example 学习过程中有以下问题或心得:

std::function<> 模板约定的签名是无法适配默认参数的:即 void handler(asio::error_code, size_t len = 0) 是无法赋值给 std::function<void(asio::error_code)> 类型的。

一方面从 c++11 才开始支持 std::function<>,另一方面在 c++03 example 中 async_accept()/async_read_some()/async_write() 竟然全部可以使用回调对象:void operator()( asio::error_code ec = asio::error_code(), std::size_t length = 0) ,要知道 async_accpet() 的回调签名可是 void(asio::error_code)

几个关键的** 伪-关键词**,其实都是宏。宏是不支持断点调试的,除非手动把所有的宏展开。不过作为成熟的网络库,这些宏都是经过千锤百炼的,调试中根本无需展开。

达夫设备的 fall-through 理解起来太难了,我能理解简单的使用案例,但前述伪-关键词还是看不懂。查看 fallthrough

成员全部交给 std::shared_ptr 托管,因为函数对象需要频繁的拷贝:一方面拷贝开销不能太大;另一方面,回调重入时成员必须有效且正确。虽然 fork server(*this)() 浅拷贝,但实际上 所有 智能指针陆续通过 ptr.reset(new xx) 全部新申请了内存,而 socket_ 的浅拷贝本就是最佳解决方案。每个主动 socket 封装都携有一份用不到 acceptor_ 也只是个瑕不掩瑜的、无法避免的小问题。

刚刚介绍的协程,不需要任何编译器/底层库的支持。只使用 C 语言本身就支持的 Duff’s Device 技术就能实现。唯一的缺点是局部变量无法跨 yield 。所以所有变量都要定义为函数对象的成员变量。 另外需要把协程定义为函数对象,需要额外编写不少代码。

更多请参考:Boost中的协程—Boost.Asio中的coroutine类

有栈协程 ☆

Boost.Context 提供的协程包括两类:非对称型协程 asymmetric_coroutine 的和对称型协程 symmetric_coroutine,前者de协程知道唤醒自己的协程,当需要暂停的时候控制流转换给那个特定的协程;对称协程中所有的协程都是相等的,协程可以把控制流给任何一个其它的协程。

c++03 examplesc++11 examplesspawn 实例演示涉及的 Boost.Coroutine 库已经 被标记为 Deprecated,推荐使用 Boost.Coroutine2。尴尬的地方在于 asio::spawn() 并未就新的 Boost.Coroutine2 封装新的实现,只能凑合着使用 deprecated 特性。

Coroutines TS Support ☆☆☆

DEMO cpp17_examples

即将进入 c++20 的协程。二百多行代码就能写个聊天室,佩服。

Coroutines TS Support,这是唯一能找到的有效介绍,虽然内容不多,但说清楚了入参、出参的用法和意义。

Support for the Coroutines TS is provided via the awaitable class template, the use_awaitable completion token, and the co_spawn() function.

co_spawn

查看 co_spawn 函数的功能与签名:通过持有协程的返回值(第二个入参)以便 resume 协程。asio 库封装的协程,协程的调度对用户都是透明的(用户只提供了某些定制点,用户无需 resume 协程),完全由 co_spawn 底层实现。类比 thread,完全是 os 的定位。

Spawn a new coroutined-based thread of execution. 生成一个新的基于协程的执行线程。

对于用户,压下好奇心,不要执着于它是如何实现的,直接理解为“新启执行线程”是直观且无误的。专注业务而非库的功能。

对称协程只是非对称协程的一个特例,我们可以通过添加一个中立的第三方调度中心的方式将非对称协程转换成对称协程(只需要在所有协程“暂停”时将控制权转交给调度中心统一调度即可)引用来源

推测 co_spawn 内部就存在这么个“调度中心”。

coroutines_ts 中给出了协程的定义等。在 asio 中协程的显著标识是函数返回值:awaitable<void> foo()

The return type of a coroutine or asynchronous operation.

所以 co_spawn() 的第二个入参使用 lambda expression 时要显式地指出返回类型:要么 return xx 语句;要么 -> ret。因为缺省使用 void

-> ret, where ret specifiers the return type. If trailing-return-type is not present, the return type is implied by the function return statements (or void if it doesn’t return any value)

总结:协程是特异的函数,特征就是返回签名;协程也不能像普通函数那样直接调用,而是通过 co_spawn() 以便 suspend 后由调度中心 resume。

executor

在协程函数体的实现中,当需要 io_context 对象时,既可以通过函数参数传进来,也可以在函数内使用以下语句获取:

1
2
// asio::any_io_executor
auto executor = co_await this_coro::executor;

一度非常困惑两者的关系,context 和 executor 分别是什么概念

在 asio 中,execution context 是一个重量级的对象,不允许拷贝。executor 是对一个轻量级的句柄(handle)对象指向对应的execution context。

另外两个特殊值,co_spawn() 的第三个参数 asio::detached,和 async_xx() 的末尾参数 asio::use_awaitable,都是固定用法。

上述三个特殊值的类型定义都是空的,模板依赖其类型进行特例化,类型的定义反而不重要。

asio::use_awaitable 用于模板特例化之外有更多的用途吗?

顺序 co_spawn() 创建 1-2-3-4 协程,在各协程中 co_await 之前的逻辑也是 1-2-3-4 顺序执行!——这是个浅显的现象,了解协程的定义和实现后,这也是理所当然的:在第一次 co_await 时函数才返回。

Special Values

This completion token that can be passed as a handler to an asynchronous operation:

  • asio::use_awaitable
  • asio::detached
  • asio::use_future

asio::use_awaitable

The use_awaitable_t class, with its value use_awaitable, is used to represent the currently executing coroutine. This completion token may be passed as a handler to an asynchronous operation. For example:

1
2
3
4
5
awaitable<void> my_coroutine()
{
std::size_t n = co_await my_socket.async_read_some(buffer, use_awaitable);
...
}

When used with co_await, the initiating function (async_read_some in the above example) suspends the current coroutine. The coroutine is resumed when the asynchronous operation completes, and the result of the operation is returned.

asio::detached

The detached_t class is used to indicate that an asynchronous operation is detached. That is, there is no completion handler waiting for the operation’s result. A detached_t object may be passed as a handler to an asynchronous operation, typically using the special value asio::detached. For example:

my_socket.async_send(my_buffer, asio::detached);

asio::use_future

The use_future_t class is used to indicate that an asynchronous operation should return a std::future object. A use_future_t object may be passed as a handler to an asynchronous operation, typically using the special value asio::use_future. For example:

1
2
std::future<std::size_t> my_future
= my_socket.async_read_some(my_buffer, asio::use_future);

The initiating function (async_read_some in the above example) returns a future that will receive the result of the operation. If the operation completes with an error_code indicating failure, it is converted into a system_error and passed back to the caller via the future.

this_coro::executor

Awaitable object that returns the executor of the current coroutine.

constexpr executor_t executor;

协程里外的异常

1
2
3
4
5
6
7
#include <asio/redirect_error.hpp>

asio::error_code ec;
co_await async_write(socket, asio::buffer(msg), asio::redirect_error(use_awaitable, ec) );
if (ec)
{
}

redirect_error_t

Completion token type used to specify that an error produced by an asynchronous operation is captured to an error_code variable.

asio::redirect_err()缺点 无伤大雅

The redirect_error token transformation recovers the option to use the error_code interface, but it suffers from the same drawbacks that make pure error codes unappealing in the synchronous case.

co_await 和线程锁

协程中禁用线程锁!

不和谐的 / 疑惑

asio::async_read() 没有 asio::use_awaitable 版本的重载 (•_•)? boost beast 提供了

The secret of stackless coroutines is that they can suspend themselves only from the top-level function.

socket::async_read_some() 无法以 asio::streambuf 作为入参

1
2
3
4
5
6
7
8
9
10
11
12
auto size = response_.m_response_buf.size();
const auto remain = content_length - size;
//auto len = co_await asio::async_read(*ssocket_, response_.m_response_buf, asio::transfer_at_least(content_length - size),
// asio::redirect_error(asio::use_awaitable, ec));
for (size_t len = 0; !ec && len < remain; )
{
auto buf = response_.m_response_buf.prepare(std::min(remain, 1024ul));
auto bytes = co_await ssocket_->async_read_some(asio::buffer(buf),
asio::redirect_error(asio::use_awaitable, ec));
response_.m_response_buf.commit(bytes);
len += bytes;
}

在 msvc2015 中断点调试无法监视变量 (ÒωÓױ)!

如何编写协程类型

请移步 coroutine 学习总结