协程是什么?
普通函数返回点:返回
协程返回点:暂停 + 返回
1 | |
如果是普通函数,待输入字符 A,执行 return 返回,该函数执行完成。往后的代码永远不会有执行的机会。
如果是协程,待输出字符 A,执行 return 返回,该函数执行完成。但下一次该函数可以继续被执行,并且从上次结束的地方继续往下执行,你可以说它有暂停功能,但也许记忆功能会更容易理解。往下执行,输出字符 A,执行 return 返回。
至于 C++20 中如何支持协程,可见此文:C++20协程入门教程。
Asio 提供的协程接口
Boost.Asio 从 C++20 开始支持协程。通过协程,可以编写异步代码,避免了嵌套的回调函数或复杂的状态机。这使得代码结构更加直观和易于维护。Boost.Asio 利用 C++ 的协程语法,使得异步操作看起来像同步操作。
常用库组件:
boost::asio::awaitable: 表示一个可以使用协程等待的类型boost::asio::use_awaitable: 用作异步操作的最后一个参数,告诉 Asio 操作返回一个awaitable对象,可以与协程配合使用co_spawn: 启动一个协程,参数分别为调度器,执行的函数,以及启动方式。比方说启动方式是 deatched,表示将协程对象分离出来,这种启动方式可以启动多个协程,他们都是独立的,如何调度取决于调度器,在用户的感知上更像是线程调度的模式,类似于并发运行,其实底层都是串行的co_await: 等待异步操作完成
boost::asio::awaitable
1 | |
作为协程或异步操作的返回类型。从定义来看,必填模板参数是返回类型 T,也就是说协程或异步操作的返回类型是什么, T 就填什么。
1 | |
第一个代表 my_async_function 返回值类型是 void ,第二个代表 my_async_function_with_result 返回值类型是 int。
boost::asio::use_awaitable
1 | |
use_awaitable 是一个标记(无参数,仅作为标记使用),告诉
Boost.Asio 返回一个 awaitable 对象,支持协程等待。
常用操作:在进行异步操作时,它作为参数传递给函数,表示希望异步结果能够与协程配合使用。
1 | |
boost::asio::co_spawn
用于启动协程的函数,它允许协程在 io_context
中运行,管理异步任务的生命周期。
1 | |
Executor:执行异步任务的执行器(通常是
io_context)。
Awaitable:一个协程,通常是返回
awaitable<T> 类型的函数。
Token:协程结束后如何处理返回值,通常使用
detached 选项表示不关心返回值;或者使用
use_future 来获取返回值。
举例说明:
1 | |
detached:表示协程完成后不需要返回值或等待结果。它用于简单的异步操作,不关心任务的结果。
use_future:协程完成后,返回一个
std::future 对象,可以等待协程的结果。
co_await
等待一个异步操作完成,通常配合 awaitable 一起使用。
比方说:
1 | |
use_future
use_future 是 co_spawn
的另一个选项,表示协程运行完成后,将返回一个 std::future
对象,可以等待并获取协程的结果。
1 | |
boost::asio::steady_timer
steady_timer 是 Boost.Asio
的定时器类,常与协程一起使用,用于执行定时任务或延迟任务。
1 | |
io_context:执行器,用于管理异步操作。
expires_after:设置定时器的超时时间。
async_wait:等待定时器超时,与协程配合使用。
异步操作函数
Boost.Asio 提供了各种异步操作函数(如 async_read,
async_write, async_connect,
async_resolve),这些函数都可以使用协程来等待结果。
1 | |
常见参数:
socket:用于通信的tcp::socket对象。buffer:用于传输数据的缓冲区,通常是boost::asio::buffer。endpoints:用于连接的地址列表。use_awaitable:表示使用协程等待异步操作的结果。
解读官方代码
我觉得结合前面接口的介绍,读懂下面的代码就不难了:
1 | |
co_spawn(io_context, listener(), detached) 。co_spawn
意味着
启动一个协程,那么需要传递三个参数,第一个参数是异步任务的执行器,网络编程中通常就是
io_context
无疑了。第二个参数就是协程,即一个函数,只不过这个函数如何才可以被视为协程,后续再聊。第三个参数有两种选择,一种选择是不关心返回值,一种是关心返回值。你可以看到我们传递的协程是没有返回值的,那就可以用
detached,表示协程完成后不需要返回值或等待结果。否则你传递 use_future
,代表协程完成后,返回一个 std::future
对象,可以等待协程的结果。
listener() 。它究竟如何是被视为一个
协程的?请看下面:
- 返回类型为
boost::asio::awaitable<T>。(必须) - 使用
co_await操作符等待异步操作。(必须) - 使用
co_return返回结果(如果有的话,我们这里是没有的)。(非必须) - 是被
boost::asio::co_spawn启动的。(必须)
listener 内部做了什么 ?
先通过this_coro::executor 获取当前协程的执行器(即
io_context 中的执行上下文),然后监听 TCP
连接。每当有客户端连接到服务器,async_accept
接收连接,并启动 echo
协程来处理该连接。这个过程是异步等待的,因为我们使用 co_await
,即co_await acceptor.async_accept(use_awaitable)。co_spawn
用于启动 echo 协程,并且使用 detached
方式运行,表示不需要等待其结束。
echo 函数就不提了,和 listener
并无二致。我主要还是提一提this_coro::executor :
获取执行器 (executor):
this_coro::executor
是一个特殊的对象,它代表当前协程正在使用的执行器(即协程是在哪个
io_context 或 executor 上运行的)。
确保异步操作正确调度: 当你通过
co_await this_coro::executor
获取执行器时,实际上是在告诉编译器,当前协程的所有异步操作都需要通过这个执行器调度。这使得协程中的
co_await 异步操作能够正确使用 executor
来调度执行。
那为什么我们主函数中没有使用呢?
在主函数中,执行器并没有直接在协程中运行,因此不需要使用
this_coro::executor。io_context
是在主函数中直接创建的,并且作为参数传递给 co_spawn。当
co_spawn 启动协程时,它会自动将 io_context
作为执行器传递给协程内部使用的 this_coro::executor。
⭐️内容取自 B 站 UP 恋恋风辰和 mmoaay 的《Boost.Asio C++ 网络编程》,仅从中取出个人以为需要纪录的内容。不追求内容的完整性,却也不会丢失所记内容的逻辑性。如果需要了解细致,建议看原视频或者读原书。