协程(Coroutines)详解
1. 协程的基本概念
- 暂停和恢复: 协程可以在某个点暂停执行,并在稍后从暂停的地方继续执行。这与传统的函数调用不同,传统函数一旦开始执行就会一直运行到结束。
- 异步编程: 协程非常适合处理异步操作,比如网络请求、文件I/O等。它们可以在等待I/O操作完成时暂停,从而避免阻塞线程。
- 轻量级: 协程比线程更轻量级,因为它们不需要操作系统的线程管理,切换上下文的开销也更小。
2. 协程的执行流程
- 启动协程 : 使用
co_spawn
启动协程,并将其提交给执行器。 - 遇到 co_await : 协程在
co_await
处暂停执行,将控制权交还给执行器。 - 异步操作继续进行: 异步操作在后台继续进行,不会阻塞线程。执行器可以调度其他任务或协程。
- 恢复执行: 当异步操作完成时,执行器将控制权交还给协程,协程从暂停的地方继续执行。
3. 关键概念和术语
co_await
- 作用: 用于在协程中等待一个异步操作完成。它会暂停协程的执行,直到被等待的操作完成,然后恢复执行。
awaitable
- 定义 : 表示可以被
co_await
等待的对象,封装了异步操作的状态和结果。 - 作用 : 与
co_await
一起使用,协程会在co_await
处暂停,直到awaitable
对象表示的异步操作完成。
co_spawn
- 作用: 启动一个新的协程。接受一个执行器(executor)、一个协程函数和一个分离策略(detached),并启动协程的执行。
执行器(executor)
- 定义 : 用于管理协程执行的对象。它负责调度协程的执行。在 Boost.Asio 中,
io_context
就是一个执行器。
use_awaitable
- 作用 : 一个标记,用于指示异步操作应该返回一个
awaitable
对象,从而可以在协程中使用co_await
来等待这些操作的完成。
4. 示例代码解析
echo
协程
cpp
awaitable<void> echo(ip::tcp::socket socket)
{
try
{
char data[1024];
for (;;)
{
std::size_t n = co_await socket.async_read_some(buffer(data), use_awaitable);
co_await async_write(socket, buffer(data, n), use_awaitable);
}
}
catch (const std::exception& e)
{
std::cout << "Exception is " << e.what() << std::endl;
}
}
- 功能: 实现一个简单的回显服务器,接收客户端发送的数据并将其原样返回。
- 流程 : 使用
co_await
等待异步读取和写入操作,通过无限循环持续处理客户端的数据,捕获并打印任何异常。
listener
协程
cpp
awaitable<void> listener()
{
auto executor = co_await this_coro::executor;
ip::tcp::acceptor acceptor(executor, { ip::tcp::v4(), 10086 });
for (;;)
{
ip::tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(executor, echo(std::move(socket)), detached);
}
}
- 功能: 负责监听端口并接受新的TCP连接。
- 流程 : 获取当前协程的执行器,创建TCP接受器,使用
co_await
等待异步接受连接,每当接受到一个新的连接时,使用co_spawn
启动一个新的echo
协程来处理该连接。
main
函数
cpp
int main()
{
try
{
io_context io_context(1);
signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait(&
{
io_context.stop();
}
);
co_spawn(io_context, listener(), detached);
io_context.run();
}
catch (const std::exception& e)
{
std::cout << "Exception is " << e.what() << std::endl;
}
}
- 功能: 程序的入口点,负责初始化I/O上下文并启动监听协程。
- 流程 : 创建
io_context
对象,设置信号处理器,捕捉SIGINT
和SIGTERM
信号,并在接收到信号时停止io_context
,使用co_spawn
启动listener
协程,调用io_context.run()
开始事件循环,处理异步操作。
5. 协程与线程的对比
- 协程: 轻量级、用户态、非抢占式,适用于I/O密集型任务。
- 线程: 重量级、内核态、抢占式,适用于CPU密集型任务。
6. 常见问题解答
- 为什么在 co_spawn 中传递 coroutine() 而不是 coroutine : 传递
coroutine()
是因为co_spawn
需要一个awaitable
对象,而不是一个可调用对象。coroutine()
调用协程函数并返回一个awaitable
对象。