C++20 引入了协程(coroutine)概念,它让异步编程变得更直观、易维护。协程本质上是一种可以“挂起”和“恢复”的函数,编译器负责将普通函数拆解成若干状态机片段,调度器则在需要时恢复它们。下面我们从基本概念到实际异步 I/O 示例,逐步剖析协程的实现方式。
1. 协程的核心概念
1.1 协程的结构
- 挂起点 (
co_await,co_yield,co_return)
协程在遇到这些关键字时会暂停执行,保存其内部状态,返回给调用者。 - 状态机
编译器把协程函数编译成一个类,内部拥有promise_type、handle、状态机逻辑。 - 协程句柄 (
std::coroutine_handle)
用于手动管理协程的生命周期,调用resume()恢复执行,destroy()结束。
1.2 promise_type
每个协程都有一个 promise_type,它定义了协程执行时的行为,例如:
struct MyPromise {
MyPromise() = default;
std::suspend_always initial_suspend() noexcept { return {}; } // 第一次挂起
std::suspend_always final_suspend() noexcept { return {}; } // 最后挂起
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
initial_suspend 与 final_suspend 可以返回 suspend_never 或 suspend_always,控制协程启动与结束时是否立即挂起。
2. 简单协程示例
2.1 计数协程
#include <coroutine>
#include <iostream>
struct Counter {
struct promise_type {
Counter 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_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
explicit Counter(std::coroutine_handle <promise_type> h) : handle(h) {}
~Counter() { if (handle) handle.destroy(); }
};
Counter count_to(int n) {
for (int i = 1; i <= n; ++i) {
std::cout << i << '\n';
co_await std::suspend_always{}; // 每次打印后挂起
}
}
调用:
auto coro = count_to(5);
coro.handle.resume(); // 1
coro.handle.resume(); // 2
// ...
3. 协程与异步 I/O
3.1 传统异步 I/O
在经典 C++ 中,异步 I/O 通常通过回调、std::future/std::promise 或第三方库(如 Boost.Asio)实现。代码往往堆叠回调,导致“回调地狱”。
3.2 协程实现异步 I/O
使用 co_await 可以把异步操作当作同步语句书写,读起来更像线性流程。下面演示基于 Boost.Asio 的异步文件读取,改写为协程:
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
namespace asio = boost::asio;
using asio::awaitable;
using asio::use_awaitable;
using asio::ip::tcp;
awaitable<std::size_t> async_read_file(const std::string& path, std::vector<char>& buffer) {
asio::io_context& ioc = co_await asio::this_coro::executor;
asio::posix::stream_descriptor fd(ioc, ::open(path.c_str(), O_RDONLY));
std::size_t total = 0;
while (true) {
std::size_t n = co_await fd.async_read_some(
asio::buffer(buffer.data() + total, buffer.size() - total), use_awaitable);
if (n == 0) break; // EOF
total += n;
}
fd.close();
co_return total; // 返回读取字节数
}
主程序
int main() {
asio::io_context ioc;
std::vector <char> buf(1024);
auto fut = async_read_file("example.txt", buf);
std::size_t bytes = fut.get(); // 这里会阻塞直到协程完成
std::cout << "Read " << bytes << " bytes.\n";
return 0;
}
关键点
co_await在async_read_file中挂起,等待 I/O 完成后恢复。- `awaitable ` 是 Boost.Asio 的协程包装器,内部包含 `promise_type`。
- 这样写法与同步 I/O 结构极为相似,避免了回调链。
4. 自定义 awaitable
如果你不想依赖第三方库,也可以手动实现一个 awaitable:
template<typename T>
struct SimpleAwaitable {
T value_;
bool ready_ = false;
std::function<void()> resume_cb_;
SimpleAwaitable(T val) : value_(val) {}
struct awaiter {
SimpleAwaitable <T>& awaitable_;
bool await_ready() { return awaitable_.ready_; }
void await_suspend(std::coroutine_handle<> h) {
awaitable_.resume_cb_ = [h](){ h.resume(); };
}
T await_resume() { return awaitable_.value_; }
};
awaiter operator co_await() { return { *this }; }
};
SimpleAwaitable <int> async_compute() {
// 模拟异步计算
int result = 42;
co_return result;
}
5. 结合 std::future 与协程
如果你想把传统 std::future 与协程结合,C++23 提供了 std::future 的 co_await 支持。示例:
std::future <int> async_square(int x) {
return std::async(std::launch::async, [x] { return x * x; });
}
awaitable <int> wrapper() {
int value = co_await async_square(5); // 自动等待 future 完成
co_return value * 2;
}
6. 性能与安全
- 堆栈:协程的挂起点只保存局部状态,实际堆栈不被压入,开销低。
- 异常:通过
promise_type::unhandled_exception()捕获异常,避免崩溃。 - 资源管理:协程句柄必须在结束时
destroy(),否则会泄漏。 - 调试:在调试时使用
-fcoroutines或对应编译器标记,查看生成的状态机代码。
7. 结语
C++20 协程为异步编程提供了“同步化”语法糖,使得代码更加可读、易维护。通过协程,你可以像写同步代码那样写异步 I/O、网络通信、任务调度等。虽然编译器会生成复杂的状态机,但对程序员而言,协程隐藏了这一层细节,让你专注于业务逻辑。未来随着标准库持续完善,协程将成为 C++ 编程不可或缺的一部分。