在现代 C++ 开发中,异步编程越来越重要,尤其是网络 I/O。传统的回调、Future、Promise 组合往往导致“回调地狱”或过度拆分代码。C++20 的协程(co_await、co_return、co_yield)提供了一种更直观、更接近同步代码的写法。本文将从概念、实现细节、实际使用三个角度阐述如何利用协程简化异步网络编程,并给出完整的示例代码。
1. 协程的核心概念
| 术语 | 说明 |
|---|---|
| 协程函数 | 使用 co_await/co_yield/co_return 的函数,返回类型是 std::generator、std::future 或自定义类型 |
| 悬挂 | 在 co_await 时,协程会暂停,控制权返回给调用者;等待异步事件完成后再恢复 |
| 恢复 | 通过事件循环或任务调度器将协程恢复,继续执行后续代码 |
| awaiter | 提供 await_ready()、await_suspend()、await_resume() 三个成员的对象,决定协程的挂起与恢复 |
协程本质上是一种“状态机”,C++ 编译器会把协程拆解为一系列状态转移,编译时产生一个隐含的结构体。使用 co_await 的地方会被拆成 “检查是否完成 → 如果未完成挂起 → 在事件完成时恢复”。
2. 异步网络编程常见难点
| 难点 | 传统解决方案 | 缺点 |
|---|---|---|
| 多层回调 | 采用回调链或链式 Future | 回调地狱、错误处理困难 |
| 状态管理 | 手动维护状态机 | 状态混乱、易出错 |
| 异常传播 | 异常捕获 + 传播 | 需要显式抛出/捕获,易漏 |
| 资源管理 | 手动打开/关闭 socket | 资源泄漏风险高 |
协程通过让代码保持线性、隐藏状态机实现,天然解决了上述问题。
3. 协程与事件循环
为了让协程真正发挥作用,需要配合一个事件循环(Event Loop)。典型实现包括:
- io_context(Boost.Asio、ASIO、libuv 等)
- 自定义 Poller(
select/poll/epoll/kqueue) - 线程池(在多线程环境下调度协程恢复)
下面给出一个简化版的事件循环框架:
class EventLoop {
public:
void run() {
while (!tasks.empty()) {
auto task = tasks.front();
tasks.pop();
task.resume(); // 恢复协程
}
}
void add_task(std::coroutine_handle<> h) {
tasks.push(h);
}
private:
std::queue<std::coroutine_handle<>> tasks;
};
在 co_await 时,await_suspend 可以把协程句柄放入事件循环队列,并注册对应的异步 I/O 事件。
4. 示例:使用协程实现简易 TCP 客户端
以下示例演示如何用 C++20 协程 + Boost.Asio 写一个简单的异步 TCP 客户端。为了保持简洁,省略了错误处理与细节检查。
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <coroutine>
#include <iostream>
#include <string>
namespace asio = boost::asio;
using asio::ip::tcp;
// 协程返回类型:异步字符串
struct awaitable_string {
struct promise_type {
std::string result;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
awaitable_string get_return_object() {
return awaitable_string{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
void return_value(std::string value) { result = std::move(value); }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> handle;
std::string value() { return handle.promise().result; }
~awaitable_string() { handle.destroy(); }
};
// awaitable 类型:等待异步 socket 读写完成
template <typename AsyncOp>
struct awaitable_op {
AsyncOp op;
std::coroutine_handle<> coro;
std::error_code ec;
std::size_t bytes_transferred;
std::suspend_always await_ready() const noexcept { return {}; }
std::suspend_always await_suspend(std::coroutine_handle<> h) {
coro = h;
op([this](auto ec, auto bytes) {
this->ec = ec;
this->bytes_transferred = bytes;
this->coro.resume(); // 恢复协程
});
return {};
}
void await_resume() { /* 这里可以检查 ec */ }
};
awaitable_op< std::function<void(std::error_code, std::size_t)> >
co_await_socket(tcp::socket& sock, std::string& buffer, std::size_t size) {
std::shared_ptr<std::vector<char>> data = std::make_shared<std::vector<char>>(size);
return awaitable_op< std::function<void(std::error_code, std::size_t)> >{
[&](auto cb) {
sock.async_read_some(asio::buffer(*data), std::move(cb));
}, nullptr, {}, 0};
}
awaitable_string async_client(asio::io_context& io) {
tcp::resolver resolver(io);
auto endpoints = co_await resolver.async_resolve("example.com", "80", asio::use_awaitable);
tcp::socket socket(io);
co_await asio::async_connect(socket, endpoints, asio::use_awaitable);
std::string request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable);
std::string response;
char data[1024];
std::size_t n = co_await socket.async_read_some(asio::buffer(data), asio::use_awaitable);
response.append(data, n);
// 简化:一次性读完
co_return response;
}
int main() {
asio::io_context io;
auto fut = async_client(io);
io.run();
std::cout << fut.value() << std::endl;
}
关键点说明
awaitable_string:包装异步结果的协程返回类型。co_await_socket:示例自定义 awaitable,使用 lambda 包装异步 I/O。async_client:整个业务流程完全线性,没有回调链。asio::use_awaitable:Boost.Asio 内置支持协程,返回一个awaitable对象,直接co_await。
5. 优点与注意事项
| 优点 | 说明 |
|---|---|
| 代码可读性 | 像同步写法,易于维护。 |
| 错误处理 | 通过异常机制统一捕获,避免回调中遗漏。 |
| 资源管理 | RAII 与协程生命周期绑定,自动释放。 |
| 并发性能 | 事件循环 + 协程天然实现高并发 I/O。 |
注意事项
- 不要在协程中长时间阻塞:仍然是同步阻塞,导致事件循环卡住。
- 协程对象:避免大对象拷贝,建议使用
std::shared_ptr或std::move。 - 异常安全:协程
promise_type的unhandled_exception必须妥善处理。 - 兼容性:不是所有库都支持协程,需查看第三方库是否提供
use_awaitable。
6. 结语
C++20 协程为异步网络编程带来了革命性的简化。通过将异步操作包装为 awaitable,我们可以用几行同步代码完成多层 I/O、错误处理与资源管理。随着编译器和标准库的不断完善,协程正逐步成为 C++ 网络编程的主流范式。希望本文能帮助你快速上手并在实际项目中尝试协程的强大力量。