在 C++20 标准中,协程(Coroutine)被正式纳入语言特性,为异步编程提供了更直观、更高效的支持。本文将从协程的基本概念入手,逐步展开 async/await 的实现细节,并讨论如何在实际项目中利用协程实现任务调度、并发 IO 与高性能网络编程。
1. 协程基础
1.1 什么是协程?
协程是一种可挂起的函数,允许在执行过程中暂停(yield)并在需要时恢复。与传统的线程或回调机制相比,协程可以:
- 保持状态:函数内部的局部变量在挂起后依然保留,继续执行时从同一状态恢复。
- 无栈分配:协程在调用时只分配一个协程句柄(
std::coroutine_handle),不需要为每个协程单独分配完整的线程栈。 - 编译器生成:C++ 编译器将协程代码转换为状态机,透明地管理挂起与恢复逻辑。
1.2 协程关键字
C++20 中引入了四个新关键字:
co_await:挂起当前协程并等待异步操作完成。co_yield:在协程内部产生一个值,类似生成器。co_return:返回协程结果。co_spawn:C++23 引入,用于启动协程。
2. async/await 的实现细节
2.1 异步操作的封装
在 C++20 中,最常用的异步容器是 std::future、std::promise,但它们是同步等待的。要真正支持 co_await,需要自定义 awaitable 对象。例如:
struct Timer {
std::chrono::milliseconds duration;
std::future <void> await() const {
return std::async(std::launch::async, [=]() {
std::this_thread::sleep_for(duration);
});
}
};
auto timer = Timer{std::chrono::milliseconds(1000)};
co_await timer; // 会挂起协程,等待1秒后恢复
2.2 awaiter 的四个必备函数
一个满足 awaitable 接口的对象,需要实现以下四个成员函数:
-
bool await_ready() noexcept;
判断是否需要挂起。若返回true,协程不挂起直接继续。 -
void await_suspend(std::coroutine_handle<> h) noexcept;
当await_ready返回false时调用。该函数负责注册恢复逻辑,例如将协程句柄加入事件循环。 -
void await_resume() noexcept;
协程恢复后调用,返回值可传递给调用者。 -
using await_resume_t = ...;
await_resume的返回类型。
2.3 简单的事件循环
下面给出一个极简的事件循环实现,用于挂起和恢复协程:
class EventLoop {
public:
void run() {
while (!tasks.empty()) {
auto task = tasks.front(); tasks.pop();
task(); // 调用协程句柄
}
}
void add_task(std::coroutine_handle<> h) {
tasks.emplace([h](){ h.resume(); });
}
private:
std::queue<std::function<void()>> tasks;
};
在 awaiter 的 await_suspend 中将协程句柄注册到 EventLoop:
void await_suspend(std::coroutine_handle<> h) noexcept override {
loop.add_task(h);
}
3. 协程在网络编程中的应用
3.1 协程 + Asio
Boost.Asio 已经支持 C++20 协程。通过 asio::awaitable,可以像编写同步代码那样书写异步网络逻辑:
asio::awaitable <void> do_echo(tcp::socket sock) {
char data[1024];
std::size_t n = co_await sock.async_read_some(asio::buffer(data), asio::use_awaitable);
co_await sock.async_write_some(asio::buffer(data, n), asio::use_awaitable);
}
3.2 高性能 TCP 服务器
借助协程,可以避免繁琐的回调嵌套,降低代码复杂度。结合 asio::strand 或自定义事件循环,能够在单线程中处理数十万连接。
asio::io_context ctx;
tcp::acceptor acceptor(ctx, tcp::endpoint(tcp::v4(), 8080));
for (;;) {
tcp::socket sock = co_await acceptor.async_accept(asio::use_awaitable);
co_spawn(ctx, do_echo(std::move(sock)), asio::detached);
}
ctx.run();
4. 协程与多任务调度
4.1 轻量级任务切换
协程句柄的挂起/恢复是 O(1) 操作,无需上下文切换成本。结合自定义调度器,可以实现:
- 优先级调度:为每个协程指定优先级,调度器根据优先级决定恢复顺序。
- 时间片轮转:为每个协程分配时间片,时间片用完自动挂起,交给下一个协程。
4.2 任务间通信
协程间可通过 std::promise/std::future、async_channel 或自定义 awaitable_queue 进行同步。一个常见模式是:
template<class T>
class AwaitableQueue {
std::queue <T> q;
std::vector<std::coroutine_handle<>> waiting;
public:
void push(T&& item) {
if (!waiting.empty()) {
waiting.front().resume();
waiting.pop_front();
} else {
q.emplace(std::forward <T>(item));
}
}
awaitable <T> pop() {
// awaiter 实现 omitted
}
};
5. 性能与内存考虑
- 栈占用:协程使用协程句柄,不再需要为每个任务分配完整栈。真正的栈空间只有当协程被挂起时才需要为其生成保存状态的栈帧。
- 抖动:在高并发场景中,频繁的挂起/恢复可能导致 cache line 抖动。可通过批量事件处理或
await_suspend内部的轻量级任务队列降低抖动。 - 异常处理:协程中使用
co_await时,需要确保异常能被正确捕获并传播。建议在顶层协程中使用 try/catch,或者在 awaiter 的await_resume中抛出异常。
6. 结语
C++20 的协程为异步编程提供了接近同步代码的可读性与可维护性,同时保持了高性能与低内存占用。掌握 awaitable 的设计模式、事件循环的实现以及协程与现有网络库(如 Asio)的结合,能够让你在大规模网络、IO 密集型应用中实现高并发、低延迟的系统。希望本文能为你在 C++ 协程之路上提供一份清晰的参考。