C++20 标准引入了协程(Coroutines),这是 C++ 语言历史上最重要的特性之一。协程提供了一种全新的异步编程范式,使得开发者可以用同步风格编写异步代码,极大地简化了回调地狱和复杂状态机的问题。本文将深入探讨 C++20 协程的核心概念、标准库支持、高级用法以及生产环境中的最佳实践。
一、什么是协程?
协程是一种可以挂起(suspend)和恢复(resume)的函数。与普通函数不同,协程在执行过程中可以暂停执行,保存当前状态,然后返回到调用者;之后可以在适当的时机从暂停点继续执行。
C++20 的协程是”无栈协程”(stackless coroutines),这意味着它们不拥有独立的调用栈。每个协程在堆上分配一个协程帧(coroutine frame)来保存挂起时的局部变量和状态,而不是像线程那样占用完整的栈空间。这使得 C++20 协程的开销极低——创建一个协程的成本与进行一次小内存分配相当。
| 特性 | 线程 | C++20 协程 |
|---|---|---|
| 调度方式 | 抢占式 | 协作式 |
| 栈空间 | 有栈(1-8MB) | 无栈(协程帧,通常几百字节) |
| 创建开销 | 高(系统调用) | 极低(一次堆分配) |
| 上下文切换 | 内核态切换 | 用户态函数调用 |
| 并发数量 | 数千 | 数十万到百万 |
| 标准支持 | C++11 起 | C++20 起 |
协程与线程并非互斥关系,而是互补关系。协程通常运行在线程之上,利用线程池来调度协程的执行。一个典型的架构是”I/O 线程池 + 协程”,即由少量线程驱动大量协程的执行。
二、C++20 协程的核心概念
C++20 协程的实现依赖于几个关键概念:promise 对象、awaitable 对象、协程句柄(coroutine handle)以及 co_await、co_yield、co_return 三个关键字。
2.1 三个新关键字
- co_await:挂起当前协程,等待某个操作完成
- co_yield:返回一个值给调用者并挂起协程
- co_return:结束协程并返回最终值
如果一个函数体中含有上述任何一个关键字,则该函数自动成为协程。
2.2 Promise 对象
每个协程都关联一个 Promise 对象,它定义协程的行为:如何创建协程结果、如何处理异常、以及协程挂起/恢复时的行为。Promise 类型通过协程返回类型的 promise_type 成员类型指定。
struct ReturnType {
struct promise_type {
ReturnType get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
2.3 协程句柄(Coroutine Handle)
协程句柄是操作协程的”遥控器”。通过它,我们可以恢复协程、销毁协程、访问协程的promise对象。std::coroutine_handle<> 是一个类型擦除的句柄,而 std::coroutine_handle<PromiseType> 则知道具体的 promise 类型。
2.4 Awaitable 对象
awaitable 是可以在 co_await 表达式中使用的对象。标准库提供了两个基本的 awaiter:
- std::suspend_never:告诉协程永远不要挂起(直接继续执行)
- std::suspend_always:告诉协程总是挂起
自定义 awaiter 需要实现三个方法:
struct MyAwaiter {
// 是否挂起?返回 true 则挂起,false 则继续
bool await_ready() const noexcept { return false; }
// 挂起后调用,返回 void 或 coroutine_handle
void await_suspend(std::coroutine_handle<> handle) {
// 保存 handle,稍后恢复
}
// 恢复时调用,返回值即为 co_await 表达式的结果
int await_resume() noexcept { return 42; }
};
三、实战:构建一个异步网络库
理论知识足够后,让我们用协程实现一个实际可用的异步 TCP 客户端。这个例子将展示协程如何让异步代码变得简洁。
3.1 封装 Task 类型
#include <coroutine>
#include <exception>
#include <functional>
#include <memory>
#include <vector>
template <typename T>
struct Task {
struct promise_type {
T result_;
std::exception_ptr exception_;
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) {
result_ = std::move(value);
}
void unhandled_exception() {
exception_ = std::current_exception();
}
};
std::coroutine_handle<promise_type> handle_;
explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Task() { if (handle_) handle_.destroy(); }
Task(Task&& other) noexcept
: handle_(std::exchange(other.handle_, {})) {}
T get_result() {
if (handle_.promise().exception_) {
std::rethrow_exception(handle_.promise().exception_);
}
return handle_.promise().result_;
}
};
3.2 事件循环
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <map>
#include <functional>
class EventLoop {
int epoll_fd_;
std::map<int, std::coroutine_handle<>> pending_;
public:
EventLoop() {
epoll_fd_ = epoll_create1(0);
}
void await_readable(int fd, std::coroutine_handle<> handle) {
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLONESHOT;
ev.data.fd = fd;
epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
pending_[fd] = handle;
}
void run() {
while (!pending_.empty()) {
struct epoll_event events[64];
int n = epoll_wait(epoll_fd_, events, 64, -1);
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
auto handle = pending_[fd];
pending_.erase(fd);
epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr);
if (handle) handle.resume();
}
}
}
~EventLoop() { close(epoll_fd_); }
};
// 全局事件循环
EventLoop& get_loop() {
static EventLoop loop;
return loop;
}
3.3 异步 Awaiter
struct ReadableAwaiter {
int fd_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
get_loop().await_readable(fd_, h);
}
void await_resume() {}
};
struct ConnectAwaiter {
int fd_;
sockaddr_in addr_;
ConnectAwaiter(int fd, sockaddr_in addr) : fd_(fd), addr_(addr) {}
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 非阻塞 connect
int rc = connect(fd_, (struct sockaddr*)&addr_, sizeof(addr_));
if (rc == 0) {
h.resume(); // 立即连接成功
return;
}
if (errno == EINPROGRESS) {
// 连接进行中,等待可写事件
struct epoll_event ev;
ev.events = EPOLLOUT | EPOLLONESHOT;
ev.data.fd = fd_;
epoll_ctl(get_loop().get_fd(), EPOLL_CTL_ADD, fd_, &ev);
get_loop().add_pending_inprogress(fd_, h);
return;
}
throw std::system_error(errno, std::system_category());
}
void await_resume() {
int err;
socklen_t len = sizeof(err);
getsockopt(fd_, SOL_SOCKET, SO_ERROR, &err, &len);
if (err != 0) throw std::system_error(err, std::system_category());
}
};
3.4 异步 TCP 客户端
#include <string>
#include <vector>
Task<std::string> async_http_get(const std::string& host,
const std::string& path) {
int sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (sock < 0) throw std::system_error(errno, std::system_category());
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, host.c_str(), &addr.sin_addr);
co_await ConnectAwaiter{sock, addr};
std::string request =
"GET " + path + " HTTP/1.1\r\n"
"Host: " + host + "\r\n"
"Connection: close\r\n\r\n";
::send(sock, request.data(), request.size(), 0);
std::string response;
char buf[4096];
while (true) {
co_await ReadableAwaiter{sock};
ssize_t n = ::read(sock, buf, sizeof(buf));
if (n <= 0) break;
response.append(buf, n);
}
close(sock);
co_return response;
}
四、高级模式:Generator 与 Lazy Evaluation
除了异步 I/O,协程的另一个重要应用场景是生成器(Generator)。co_yield 关键字使得创建惰性求值的数列变得非常简单。
#include <coroutine>
#include <optional>
template <typename T>
struct Generator {
struct promise_type {
T current_value_;
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
std::suspend_always yield_value(T value) {
current_value_ = std::move(value);
return {};
}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
Generator(auto h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
Generator(Generator&& other) noexcept
: handle_(std::exchange(other.handle_, {})) {}
bool next() {
if (!handle_ || handle_.done()) return false;
handle_.resume();
return !handle_.done();
}
T value() { return handle_.promise().current_value_; }
};
// 使用生成器生成斐波那契数列
Generator<size_t> fibonacci() {
size_t a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// 使用示例
void print_fibonacci(int n) {
auto fib = fibonacci();
for (int i = 0; i < n && fib.next(); ++i) {
std::cout << fib.value() << " ";
}
// 输出: 0 1 1 2 3 5 8 13 21 34
}
五、C++23 对协程的增强
C++23 在协程生态方面做了重要的改进,主要包括 std::generator 标准生成器和 std::print 格式化输出支持。
5.1 std::generator(C++23)
C++23 标准库引入了 std::generator<T>,使得编写生成器不再需要手动实现 promise_type:
#include <generator>
#include <ranges>
std::generator<int> squares(int n) {
for (int i = 0; i < n; ++i) {
co_yield i * i;
}
}
// 直接用于 range-based for 循环
for (int x : squares(10)) {
std::print("{} ", x);
}
// 输出: 0 1 4 9 16 25 36 49 64 81
5.2 与 Ranges 库的集成
std::generator 实现了 range 接口,可以直接与标准库中的 range adapter 配合使用:
#include <generator>
#include <ranges>
#include <algorithm>
std::generator<int> range(int start, int end, int step = 1) {
for (int i = start; i < end; i += step) {
co_yield i;
}
}
int main() {
auto values = range(0, 100)
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 3; })
| std::views::take(10);
for (int v : values) {
std::print("{} ", v);
}
// 输出: 0 6 12 18 24 30 36 42 48 54
}
六、生产环境最佳实践
在生产环境中使用 C++20 协程时,有几个关键点需要特别注意。
6.1 性能优化
- 避免不必要的堆分配:C++20 标准允许编译器在某些情况下省略协程帧的堆分配(通过
HALO优化)。将协程定义为inline或在同一翻译单元中定义调用方和被调用方可以提高优化机会。 - 使用 custom allocator:为协程帧提供自定义分配器可以显著减少内存分配开销,特别是在高并发场景下。
- 注意 initial_suspend:如果协程在初始挂起后立即需要被恢复,使用
std::suspend_never可以避免一次不必要的挂起-恢复开销。 - 小心 final_suspend:
final_suspend必须是noexcept,且如果使用std::suspend_never,协程框架会在协程完成后自动销毁。使用std::suspend_always则需要在外部显式销毁。
6.2 内存安全与生命周期
- 悬垂引用问题:协程挂起后,函数中的局部变量会被保存在协程帧中。但如果协程帧被销毁(例如协程句柄被提前释放),则保留的引用和指针都会失效。
- 协程句柄所有权:明确谁拥有
std::coroutine_handle的所有权。常见模式是将协程句柄包裹在智能指针或 RAII 包装器中。 - 异常安全:Promise 的
unhandled_exception方法负责处理逃逸出协程体的异常。一定要将异常存储起来,在获取结果时重新抛出。
6.3 调试技巧
- 启用 AddressSanitizer:编译器参数
-fsanitize=address可以帮助检测协程相关的内存错误。 - 使用 libc++ 的调试支持:Clang 的 libc++ 提供了对
std::coroutine_handle的调试断言。 - 日志跟踪:在
await_suspend和await_resume中添加日志,可以清晰看到协程的执行流程。 - GDB 支持:GDB 10+ 开始支持打印
std::coroutine_handle的状态,可以查看协程是否完成。
6.4 与第三方库的集成
许多现代 C++ 库已经提供了对 C++20 协程的支持:
| 库名称 | 协程支持 | 说明 |
|---|---|---|
| Boost.Asio | ✅ 完全支持 | 提供了 co_spawn、asio::use_awaitable |
| libuv | ✅ 通过 uvw 封装 | 需要自定义 awaiter 封装 |
| cppcoro | ✅ 原生协程库 | 提供了 Task、Generator、AsyncGenerator 等 |
| folly | ✅ 完全支持 | Facebook 的 folly::coro 命名空间 |
| libco | ❌ 不兼容 | 腾讯的协程库,基于有栈协程 |
七、协程 vs 传统异步模式
让我们用一个实际场景来对比协程和其他异步编程方式的差异:同时发送多个 HTTP 请求并聚合结果。
传统回调方式(假代码):
void fetch_all(std::vector<std::string> urls, Callback callback) {
auto results = std::make_shared<std::vector<std::string>>(urls.size());
auto counter = std::make_shared<int>(0);
for (size_t i = 0; i < urls.size(); ++i) {
http_get_async(urls[i], [i, results, counter, callback](std::string r) {
(*results)[i] = r;
if (++(*counter) == results->size()) {
callback(*results);
}
});
}
}
// 错误处理?几乎不可能优雅实现
协程方式:
Task<std::vector<std::string>> fetch_all(std::vector<std::string> urls) {
std::vector<std::string> results;
results.reserve(urls.size());
for (const auto& url : urls) {
// 这里可以并发启动多个协程
results.push_back(co_await async_http_get(url));
}
co_return results;
}
Task<std::vector<std::string>> fetch_all_parallel(
std::vector<std::string> urls) {
std::vector<Task<std::string>> tasks;
for (const auto& url : urls) {
tasks.push_back(async_http_get(url));
}
std::vector<std::string> results;
for (auto& t : tasks) {
results.push_back(t.get_result());
}
co_return results;
}
协程版本的代码量减半,逻辑清晰,错误处理自然(使用 try-catch),而且可以轻松从串行切换为并行。
八、总结与展望
C++20 协程为 C++ 带来了真正现代的异步编程能力。虽然学习曲线相对陡峭——需要理解 promise_type、awaitable、coroutine_handle 等概念——但一旦掌握,它能极大提升异步代码的可读性和可维护性。
目前 C++20 协程的主要痛点在于:
- 标准库支持仍然有限(C++23 才加入
std::generator) - 没有标准的 async Task 类型(需要第三方库或自行实现)
- 异常处理的 boilerplate 较多
- 编译器支持仍不完全一致(GCC、Clang、MSVC 各有差异)
展望 C++26,预计会进一步改进协程的支持:引入 std::async_scope 作为标准异步作用域,更好支持结构化并发的 fire-and-forget 和结构化任务。协程在 C++ 中的位置将越来越重要,成为高性能服务器开发、游戏引擎、实时系统等领域的标配技术。
对于团队来说,建议从非关键路径开始尝试协程,比如日志记录、指标收集、配置文件加载等辅助功能。随着经验积累,再逐步将其推广到核心业务逻辑中。配合现代 C++ 的其他特性(如 Concepts、Ranges、Modules),C++20/23 正在将 C++ 带入一个新的黄金时代。
汤不热吧