C++20 引入了 coroutine(协程)这一强大的语法特性,彻底改变了我们对异步编程的思考方式。传统的异步实现往往依赖回调、事件循环或线程池,代码可读性差、错误率高,而协程通过让函数“挂起”和“恢复”,让异步流程像同步流程一样直观。本文将从概念入手,逐步拆解协程的实现细节,并给出实战示例,帮助读者快速掌握并在项目中落地。
1. 协程基础概念
- 挂起点(Suspend Point):在协程中,
co_await、co_yield、co_return等关键字会产生挂起点,函数在此处暂停执行。 - 协程句柄(Coroutine Handle):每个协程都有一个句柄,用来管理其生命周期、恢复执行以及访问结果。句柄类型通常为
std::coroutine_handle<>。 - 悬挂对象(Suspension Object):
co_await后面跟随的对象负责决定协程是否挂起以及挂起时的行为。常见的悬挂对象有std::suspend_always、std::suspend_never等。
2. 协程的基本语法
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task foo() {
std::cout << "开始\n";
co_await std::suspend_always{}; // 挂起点
std::cout << "恢复\n";
}
int main() {
foo(); // 仅创建协程,实际未执行
return 0;
}
promise_type:每个协程必须实现一个promise_type,它定义了协程生命周期中的行为(挂起、返回、异常处理等)。initial_suspend与final_suspend:分别控制协程开始前和结束后的挂起行为。
3. 典型应用场景
3.1 异步 I/O
使用 co_await 等待底层 I/O 完成,避免回调地狱。例如结合 Boost.Asio 的 awaitable:
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
boost::asio::awaitable <void> async_read(boost::asio::ip::tcp::socket& sock) {
char buf[1024];
std::size_t n = co_await sock.async_read_some(boost::asio::buffer(buf), boost::asio::use_awaitable);
std::cout << "收到数据: " << std::string(buf, n) << "\n";
}
3.2 并行流水线
将多个协程串联起来,形成数据处理流水线,天然支持异步等待与并行执行。
struct Frame {
int id;
// 其它数据...
};
Frame decode(const std::vector <char>& raw) { /* ... */ }
boost::asio::awaitable <Frame> process_frame(const std::vector<char>& raw) {
Frame f = co_await boost::asio::async_invoke(decode, raw, boost::asio::use_awaitable);
// 进一步处理...
co_return f;
}
4. 协程与线程的区别
| 维度 | 线程 | 协程 |
|---|---|---|
| 开销 | 高(上下文切换、堆栈管理) | 低(协程上下文仅为少量寄存器与状态) |
| 可读性 | 难以直观展示异步流程 | 如同步流程,易维护 |
| 并发模型 | OS 调度 | 由程序员手动调度或库实现 |
| 互斥 | 需要锁 | 可通过 std::atomic、awaitable 处理 |
5. 实战:基于协程的简易 HTTP 服务器
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
using asio::awaitable;
awaitable <void> handle_session(tcp::socket socket) {
char data[4096];
std::size_t n = co_await socket.async_read_some(asio::buffer(data), asio::use_awaitable);
std::string request(data, n);
std::cout << "请求: " << request << "\n";
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world";
co_await asio::async_write(socket, asio::buffer(response), asio::use_awaitable);
}
awaitable <void> server(asio::io_context& ctx, unsigned short port) {
tcp::acceptor acceptor(ctx, tcp::endpoint(tcp::v4(), port));
while (true) {
tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
asio::co_spawn(ctx, handle_session(std::move(socket)), asio::detached);
}
}
int main() {
asio::io_context ctx;
asio::co_spawn(ctx, server(ctx, 8080), asio::detached);
ctx.run();
}
co_spawn:把协程放入事件循环中执行。asio::use_awaitable:让async_*函数返回awaitable,可直接co_await。
6. 常见坑与调试技巧
-
协程句柄泄露
co_await后返回的句柄若未显式销毁,可能导致资源泄露。常用的做法是使用std::unique_ptr或std::coroutine_handle<>::destroy()。 -
异常传播
协程内部异常需在promise_type::unhandled_exception()处理,否则会终止程序。可使用std::exception_ptr保存异常并在外层co_await时重新抛出。 -
调试
- 通过
-g编译,使用 GDBinfo coroutine查看协程状态。 - 结合
asio::debug打印事件循环日志。
- 通过
7. 结语
C++20 协程的出现,是语言演进中的一次重大跃迁。它将异步编程的“隐式”变为“显式”,使代码可读性大幅提升,错误率显著下降。掌握协程的核心概念与实践技巧后,你将能更轻松地实现高性能、低延迟的网络服务、并发计算以及复杂事件驱动系统。希望本文能为你打开协程世界的大门,开启更高效、更优雅的编程旅程。