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 源码,可以学到“错误代码 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
运算符使用时,更是得不到任何提示信息。只能“瞎写”参数数目和类型,编译报错 + 查看源码多次试验究竟怎么写入参。
结合 c++11 std::future
使用 asio 的异步接口,只是在局部提升效率:每个异步接口返回的 future
,在其他异步接口中使用时要传入 future::get()
结果,**而非 future
**。从全局来看依旧是阻塞的。
当然,有些业务场景恰好如此。一方面想改进每次同步 IO 的阻塞,因为两次 IO 之间可以做些其余工作;另一方面,下一次 IO 调用依赖前一次 IO 的结果。如果为了局部改进,整体改用「异步 + 回调」形式,开发耗时久:“回调地狱”真的很累,难写,看的人也累。
在上述场景中,如果没有 两次 IO 之间可以做些其余工作 的需求,其实再次“退化”改用同步接口即可。
协程
Boost ASIO supports 3 coroutine types:
- Stackless Coroutines - Boost ASIO own light weight co routine library using pre-processor macros (been around for a long time now)
- Stackful Coroutines - Uses Boost Coroutine library
- Coroutines TS Support (experimental) - Uses CO Routine TS (which is also stackless)
无栈协程 ☆
Boost 库中的协程支持两种方式:
- 一种是封装了
Boost.Coroutine
的spawn
,是一个 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 examples 和 c++11 examples 中 spawn
实例演示涉及的 Boost.Coroutine
库已经 被标记为 Deprecated,推荐使用 Boost.Coroutine2
。尴尬的地方在于 asio::spawn()
并未就新的 Boost.Coroutine2
封装新的实现,只能凑合着使用 deprecated 特性。
- 如何使用 Boost.Coroutine2,请参考:Boost中的协程—Boost.Coroutine2
- 如何通过
asio::spawn()
使用 deprecated 的 Boost.Coroutine,请参考:在 Boost.Asio 中使用协程
Coroutines TS Support ☆☆☆
DEMO cpp17_examples
即将进入 c++20 的协程。二百多行代码就能写个聊天室,佩服。
Coroutines TS Support,这是唯一能找到的有效介绍,虽然内容不多,但说清楚了入参、出参的用法和意义。
Support for the Coroutines TS is provided via the
awaitable
class template, theuse_awaitable
completion token, and theco_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 (orvoid
if it doesn’t return any value)
总结:协程是特异的函数,特征就是返回签名;协程也不能像普通函数那样直接调用,而是通过 co_spawn()
以便 suspend 后由调度中心 resume。
executor
在协程函数体的实现中,当需要 io_context
对象时,既可以通过函数参数传进来,也可以在函数内使用以下语句获取:
1 | // asio::any_io_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 | awaitable<void> my_coroutine() |
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 | std::future<std::size_t> my_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 |
|
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 | auto size = response_.m_response_buf.size(); |
在 msvc2015 中断点调试无法监视变量 (ÒωÓױ)!
如何编写协程类型
请移步 coroutine 学习总结