在 C++20 之后,协程(coroutine)正式成为标准库的一部分,为异步编程提供了更为简洁直观的语法。相比传统的回调链或多线程阻塞方式,协程可以让我们以同步的写法来描述异步流程,极大降低了错误率并提升了可维护性。本文将从协程的基本概念出发,结合 std::experimental::filesystem、asio、Boost.Asio 等常见库,展示如何在实际项目中实现高效的异步 I/O。
1. 协程的基本原理
协程本质上是一种能够暂停并恢复执行的函数。C++ 协程需要以下几个核心元素:
| 关键词 | 作用 |
|---|---|
co_await |
暂停协程,等待一个可等待对象(awaitable)完成后恢复 |
co_yield |
在协程内产生值,暂停执行,将值返回给调用者 |
co_return |
结束协程,返回最终结果 |
协程的返回类型不再是普通的 void 或 int,而是一个 awaiter,它需要实现 await_ready、await_suspend、await_resume 三个成员函数。C++ 标准库提供了 std::future、std::promise、std::experimental::generator 等常用 awaitable。
2. 协程与 I/O 的结合
2.1 传统 I/O 方式
std::ifstream file("data.txt");
if (!file) { /* 处理错误 */ }
std::string line;
while (std::getline(file, line)) {
process(line);
}
在单线程模型中,文件读取是阻塞的;在多线程环境下,需要显式使用 std::thread 或 std::async,并且还要处理线程同步与异常。
2.2 异步 I/O:Boost.Asio 的 async_read_until
boost::asio::ip::tcp::socket socket(io_context);
socket.async_read_until(buffer, '\n',
[](boost::system::error_code ec, std::size_t length) {
if (!ec) process(buffer.data(), length);
});
io_context.run();
这种方式基于事件循环,使用回调来处理完成事件。虽然可扩展性好,但代码可读性较差,错误处理容易被遗漏。
2.3 使用协程简化异步 I/O
借助 C++20 的协程和 Asio 的协程扩展(boost::asio::awaitable),可以将上述代码改写为:
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/detached.hpp>
using namespace boost::asio;
using namespace boost::asio::ip;
awaitable <void> async_read_lines(tcp::socket& sock) {
std::string line;
std::size_t n = 0;
while ((n = co_await async_read_until(sock, dynamic_buffer(line), '\n', use_awaitable)) > 0) {
process(line);
line.clear();
}
}
int main() {
io_context ctx;
tcp::resolver resolver(ctx);
auto endpoints = resolver.resolve("example.com", "http");
tcp::socket socket(ctx);
async_connect(socket, endpoints, use_awaitable).get();
async_read_lines(socket).get();
}
此代码保持了同步编程的直观性,同时利用协程的挂起/恢复机制实现异步 I/O。async_connect、async_read_until 等函数返回 awaitable,可以直接使用 co_await。
3. 协程的性能与细节
- 栈大小:C++ 协程的栈由编译器决定,通常为 8KB 左右,足够处理大部分业务逻辑。但若在协程中使用大量局部对象,可能导致栈溢出。可使用
std::vector或动态分配来缓解。 - 异常传播:协程的
co_return可以抛出异常,调用方可以捕获。若使用awaitable, 需要在co_await时使用try-catch包裹。 - 内存分配:协程内部会产生一些对象(awaiter、promise)。使用
co_return的时候,C++17 规定使用std::allocator的默认分配器,性能已可接受。若对性能极致要求,可考虑自定义分配器。
4. 协程与现有框架的整合
4.1 与 Qt
Qt 5.15 之后引入了 QFuture 的协程支持。可以通过 QFutureWatcher 与 co_await 配合,实现异步任务的统一管理。
QFuture <void> fut = QtConcurrent::run([]{ heavy_task(); });
co_await fut;
4.2 与 Unreal Engine
Unreal Engine 5 支持 C++20 协程,使用 async 宏可以在蓝图和 C++ 代码间进行异步调用。利用协程可以让 AI、物理计算等模块更易维护。
5. 小结
- 协程为 C++ 提供了一种优雅的异步编程模型,兼具同步代码的可读性与异步 I/O 的高效性。
- 与 Asio 等库配合使用,可轻松实现高性能网络服务。
- 在实际项目中,应根据业务需求选择合适的异步框架,并注意协程的栈管理、异常处理与内存分配细节。
通过学习协程与异步 I/O 的结合,开发者可以在 C++ 生态中构建更加现代、可维护的高性能应用。