C++20在标准库中引入了协程(coroutine)这一强大的异步编程工具,为编写高性能、低延迟的异步代码提供了更直观、更低级别的控制。本文将从协程的基本概念开始,结合实际案例,系统阐述协程的核心机制、实现细节以及常见的使用场景。
一、协程的基本概念
协程是一种轻量级的“挂起/恢复”函数,能够在执行过程中随时挂起,等到需要时再恢复执行。它与传统的线程或进程相比,拥有更小的栈空间、快速的切换以及更可读的代码结构。
1.1 协程的生命周期
- 创建:协程对象(如
std::coroutine_handle<>)被生成并绑定到实际的协程函数。此时协程函数的入口点被设置,但尚未执行。 - 挂起:通过
co_await、co_yield或co_return,协程可以将控制权交回调用者,等待下一次恢复。 - 恢复:调用者通过
handle.resume()将协程恢复到下一个挂起点。 - 结束:协程执行到
co_return或抛出异常后,协程资源被销毁。
1.2 awaitable 与 awaiter
awaitable:协程中可等待的对象,提供await_ready、await_suspend、await_resume三个成员函数。标准库提供了std::future、std::async等已实现的 awaitable。awaiter:由awaitable的operator co_await返回,负责挂起/恢复流程。
二、协程的实现细节
2.1 协程句柄与堆栈
C++ 协程通过 std::coroutine_handle<> 对象来管理协程的状态。协程的堆栈通常分为两部分:
- 框架堆栈:由编译器生成,用于存放局部变量、返回地址等。
- 协程堆栈:在
operator new时动态分配,包含协程的执行上下文。
2.2 协程框架(State Machine)
编译器将协程转换为一个状态机。每个 co_await、co_yield 等挂起点对应一个状态,调度器根据当前状态决定是否需要挂起或恢复。例如:
task <int> async_add(int a, int b) {
co_await std::suspend_always{}; // 第1个挂起点
int result = a + b;
co_return result; // 终止
}
编译器会生成类似以下结构:
struct async_add_state {
int a, b;
int result;
int state; // 0: 未开始, 1: 等待, 2: 完成
};
2.3 对象生命周期管理
由于协程堆栈可能跨越多个协程调用,必须小心资源释放。C++20 提供 std::suspend_always 和 std::suspend_never,以及 co_return 的析构逻辑,帮助开发者控制资源清理。
三、实战案例:异步文件读取
以下代码演示了如何使用协程实现一个简单的异步文件读取器。示例基于 POSIX 事件轮询(epoll),但核心思想可迁移到任何 I/O 多路复用框架。
#include <coroutine>
#include <iostream>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <vector>
#include <cstring>
struct async_read {
struct promise_type {
std::coroutine_handle<> continuation;
int fd;
char* buffer;
size_t size;
ssize_t result;
async_read get_return_object() {
return { std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept {
return {};
}
void return_value(ssize_t val) { result = val; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
async_read(std::coroutine_handle <promise_type> h) : handle(h) {}
~async_read() { if (handle) handle.destroy(); }
// 启动协程
void start() {
handle.resume();
}
// 等待结果
ssize_t get() { return handle.promise().result; }
};
struct await_readable {
int fd;
std::coroutine_handle<> continuation;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
continuation = h;
// 注册到 epoll
int epfd = ::epoll_create1(0);
struct epoll_event ev{ .events = EPOLLIN, .data.fd = fd };
::epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
// 等待就绪
::epoll_wait(epfd, &ev, 1, -1);
::close(epfd);
continuation.resume();
}
void await_resume() const noexcept {}
};
async_read read_file_async(int fd, char* buffer, size_t size) {
await_readable readable{ fd };
co_await readable;
ssize_t n = ::read(fd, buffer, size);
co_return n;
}
int main() {
int fd = ::open("test.txt", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
char buf[1024];
auto reader = read_file_async(fd, buf, sizeof(buf));
reader.start(); // 启动协程
ssize_t n = reader.get(); // 获取结果
std::cout << "Read " << n << " bytes: " << std::string(buf, n) << '\n';
::close(fd);
return 0;
}
3.1 代码说明
async_read:封装协程的 promise 和 handle,提供start()和get()接口。await_readable:实现awaitable,在await_suspend中使用epoll监视文件描述符可读事件。协程挂起,等到事件发生后再恢复。read_file_async:协程函数,首先挂起等待文件可读,然后调用read并返回读取字节数。
该示例展示了协程与传统回调、事件循环相比的优点:代码更线性、易于维护,并且协程切换的开销极小。
四、协程常见陷阱
- 无限挂起:若协程未正确
co_return或co_yield,调用resume()将导致死循环。始终确保有退出路径。 - 资源泄露:协程对象持有资源(如文件描述符、内存),若协程异常退出,需确保
promise_type::unhandled_exception或自定义析构释放。 - 与多线程混用:协程本身是单线程的,若在多线程中调用
resume(),需使用同步机制避免竞争。
五、总结
C++20 的协程为异步编程提供了强大的工具,既能保持代码的同步式可读性,又能获得类似事件循环的性能优势。通过理解协程的生命周期、实现细节以及常见陷阱,开发者可以在实际项目中灵活运用协程,实现更高效、更易维护的异步代码。祝你编码愉快!