C++20 引入了协程(Coroutines),为异步编程提供了更直观、更高效的语法。本文将从协程的基本概念入手,演示如何在实际项目中使用协程,以及在性能和可维护性方面的最佳实践。
1. 协程的基本概念
协程是一种轻量级的用户级线程,能够在执行过程中暂停(co_await、co_yield 或 co_return)并在之后恢复。与传统的回调或 std::future 相比,协程在语义上更接近同步代码,易于阅读和调试。
核心关键字:
co_await:等待一个awaitable对象完成。co_yield:将值返回给调用者,协程暂停。co_return:结束协程,返回最终结果。
2. 基础协程框架
下面给出一个最小化的协程实现,演示 generator 的工作机制:
#include <coroutine>
#include <iostream>
template <typename T>
struct Generator {
struct promise_type {
T value_;
std::suspend_always yield_value(T value) {
value_ = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() {
return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle_;
explicit Generator(std::coroutine_handle <promise_type> h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
struct Iterator {
std::coroutine_handle <promise_type> h_;
Iterator(std::coroutine_handle <promise_type> h) : h_(h) {}
Iterator& operator++() { h_.resume(); return *this; }
T operator*() const { return h_.promise().value_; }
bool operator==(std::default_sentinel_t) const { return !h_ || h_.done(); }
};
Iterator begin() { return Iterator{handle_}; }
std::default_sentinel_t end() { return {}; }
};
生成器函数:
Generator <int> count_to(int n) {
for (int i = 1; i <= n; ++i)
co_yield i;
}
使用方式:
for (int i : count_to(5))
std::cout << i << " ";
3. 协程与异步 I/O
在网络编程中,协程可以大幅简化异步 I/O。下面演示如何使用 asio 的 awaitable 与协程配合:
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
namespace asio = boost::asio;
using asio::awaitable;
using asio::ip::tcp;
awaitable <void> echo(tcp::socket socket) {
char data[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(
asio::buffer(data), asio::use_awaitable);
co_await asio::async_write(socket, asio::buffer(data, n),
asio::use_awaitable);
}
}
服务器入口:
int main() {
asio::io_context io{1};
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 12345));
co_spawn(io, [&]() -> awaitable <void> {
for (;;) {
auto socket = co_await acceptor.async_accept(asio::use_awaitable);
co_spawn(io, echo(std::move(socket)), asio::detached);
}
}, asio::detached);
io.run();
}
此代码几乎与同步版本的结构相同,但在内部利用了协程实现非阻塞 I/O。
4. 性能考量
- 协程上下文切换:协程切换是非常轻量的,仅涉及保存/恢复栈帧指针,几乎不需要额外的栈空间。
- 栈使用:协程默认使用“栈片”技术,自动按需扩展栈,减少内存占用。
- 异常传播:协程的异常传播通过
promise_type::unhandled_exception完成,易于捕获和记录。
对比传统 std::future,协程在多任务场景下能降低线程数、减少锁竞争,从而提升吞吐量。
5. 编写高质量协程的最佳实践
| 规则 | 说明 |
|---|---|
| 明确返回类型 | 使用 `awaitable |
或自定义generator,避免使用void`。 |
|
| 避免深层嵌套 | 过多层 co_await 可能导致错误难以追踪,保持层级浅显。 |
| 异常安全 | 在 promise_type 中实现 unhandled_exception,及时记录异常。 |
| 资源管理 | 与普通 RAII 结合使用,确保资源在协程结束时被释放。 |
| 可读性 | 与同步代码保持一致的结构,减少学习成本。 |
6. 小结
C++20 的协程为异步编程提供了天然的、面向流的编程模型。通过掌握基本的协程语法、结合 asio 等库实现高效的 I/O,开发者可以在不牺牲可读性的前提下,大幅提升程序的性能和可维护性。下一步可以尝试将协程与任务调度器(如 std::thread_pool)结合,进一步构建可扩展的服务器框架。祝你编码愉快!