协程(Coroutines)是 C++20 引入的一项强大功能,它允许你在函数内部暂停和恢复执行,从而简化异步编程、生成器、状态机等场景。下面我们从概念、语法、实现原理、常见用法和性能考虑等角度,系统地剖析协程。
一、协程的基本概念
-
暂停与恢复
协程不像线程那样完全隔离;它在同一个线程内运行,但能在指定点挂起(co_await、co_yield、co_return)并在之后恢复。挂起点保留了局部变量状态,真正的上下文切换发生在编译器生成的状态机中。 -
协程函数
`(C++23)和自定义的 `task`。
在 C++20 中,协程函数的返回类型必须是支持promise_type的类型。最常见的有 `std::generator -
协程句柄
std::coroutine_handle是对协程内部实现的可操作句柄,允许手动调度、检查完成状态等。
二、语法与实现细节
1. 关键字
| 关键字 | 用途 |
|---|---|
co_await |
等待另一个协程或异步操作完成,暂停当前协程 |
co_yield |
在生成器中产生一个值,暂停当前协程 |
co_return |
结束协程并返回值(如 void 或 int 等) |
co_await 与 co_yield 需要配合 awaitable / generator 类型 |
2. Promise 类型
协程函数返回的类型必须具备内部的 promise_type,它定义了协程运行时的生命周期函数:
struct MyTask {
struct promise_type {
MyTask get_return_object() {
return MyTask{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(int v) { value = v; }
void unhandled_exception() { std::terminate(); }
int value;
};
std::coroutine_handle <promise_type> h;
};
3. 状态机生成
编译器会把协程函数编译成一个状态机,每个 co_await/co_yield 对应一个状态点。状态机内部维护 promise_type,并在挂起时保存局部变量。
三、常见使用场景
1. 异步 I/O
结合 std::experimental::filesystem::file、网络库(如 Boost.Asio 或 libuv):
task <void> async_read(std::string path) {
auto buffer = std::make_shared<std::vector<char>>(1024);
auto fd = co_await open_async(path, O_RDONLY);
size_t n = co_await read_async(fd, buffer.get(), buffer->size());
co_return;
}
2. 生成器
实现无限序列(斐波那契数列):
generator <int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int tmp = a + b;
a = b; b = tmp;
}
}
3. 状态机
用协程实现网络协议解析:
task <void> parse_protocol(stream& s) {
while (true) {
auto header = co_await s.read_exact(sizeof(Header));
if (!header) co_return; // 关闭连接
auto body = co_await s.read_exact(header.length);
process(body);
}
}
四、性能与限制
| 项目 | 说明 |
|---|---|
| 栈占用 | 协程不需要单独线程栈,堆上存储局部变量,内存更小 |
| 上下文切换 | 由于是状态机,切换开销低于线程切换,但比单线程同步略高 |
| 异常传播 | 通过 promise_type::unhandled_exception,异常会在协程恢复点抛出 |
| 标准库支持 | C++23 引入 std::generator、std::task,更方便使用 |
| 兼容性 | 需要 C++20/23 编译器,部分老旧编译器尚不完全支持 |
五、实战案例:协程驱动的轻量级 HTTP 服务器
#include <iostream>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <coroutine>
struct task {
struct promise_type { /* ... 省略实现 ... */ };
};
task handle_client(int fd) {
char buf[1024];
while (true) {
ssize_t n = co_await async_read(fd, buf, sizeof(buf));
if (n <= 0) break;
// 简单响应
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world";
co_await async_write(fd, response.c_str(), response.size());
}
close(fd);
}
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 省略 bind / listen 代码
int epoll_fd = epoll_create1(0);
struct epoll_event ev{ .events = EPOLLIN, .data.fd = server_fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
while (true) {
struct epoll_event events[10];
int n = epoll_wait(epoll_fd, events, 10, -1);
for (int i = 0; i < n; ++i) {
if (events[i].data.fd == server_fd) {
int client_fd = accept(server_fd, nullptr, nullptr);
handle_client(client_fd); // 协程启动
}
}
}
}
该服务器使用 epoll + 协程实现事件驱动,避免了线程池开销,能够处理成千上万并发连接。
六、学习路径建议
- 先掌握 C++20 基础(编译器特性、标准库)
- 了解协程的语义:
co_await、co_yield、co_return - 阅读官方实现:
libc++、libstdc++中协程实现源码 - 动手实践:从生成器、任务调度到网络 I/O
- 关注性能:剖析协程状态机生成、内存占用、异常路径
七、结语
协程是 C++20/23 的重要里程碑,为高并发、低延迟的应用开发提供了更简洁的模型。掌握协程不仅能提升代码可读性,还能在性能上获得显著优势。未来随着标准库的完善,协程将在异步编程领域扮演更加核心的角色。祝你在 C++ 的协程世界里玩得开心!