在C++20中,协程(coroutine)被正式加入标准库,提供了一种轻量级的异步编程模型。相比传统的回调或多线程,协程可以在单线程中实现高效、可读性强的异步代码。本文将从协程的基本概念开始,逐步展示如何在实际项目中使用协程,并解决常见问题。
一、协程基础
1. 协程是什么?
协程是一种程序执行单元,支持在任意位置挂起(co_await、co_yield、co_return)并在以后恢复。它们的特点是:
- 轻量级:不像线程那样需要堆栈和调度开销。
- 可暂停:执行可以在任何点暂停,等待某个事件。
- 易读性:代码流类似同步代码,避免回调地狱。
2. 关键字
co_await:等待一个可等待对象(awaitable),类似await。co_yield:生成一个值,类似生成器。co_return:返回一个值并结束协程。
3. Awaitable 类型
要让对象可以被 co_await,必须满足以下接口:
bool await_ready(); // 是否立即完成
void await_suspend(std::coroutine_handle<>) ; // 挂起
auto await_resume(); // 结果
二、协程的实现
1. 自定义 awaitable
下面实现一个简单的异步延时:
struct Sleep {
std::chrono::milliseconds dur;
bool await_ready() const noexcept { return dur.count() <= 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, dur = dur]{
std::this_thread::sleep_for(dur);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
Sleep sleep_for(std::chrono::milliseconds ms) {
return Sleep{ms};
}
2. Coroutine Promise
协程需要一个 promise 对象,用来管理返回值和异常。最常见的是 std::future/std::promise 的组合,但 C++20 提供了更通用的 std::generator 和 std::async 结构。
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>
template<typename T>
struct Task {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T value_;
std::exception_ptr ex_;
Task get_return_object() {
return Task{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { ex_ = std::current_exception(); }
void return_value(T v) { value_ = v; }
// 可选:允许 co_await
T await_resume() {
if (ex_) std::rethrow_exception(ex_);
return value_;
}
};
handle_type h_;
explicit Task(handle_type h) : h_(h) {}
~Task() { if (h_) h_.destroy(); }
T get() {
if (!h_.done()) h_.resume();
return h_.promise().await_resume();
}
};
3. 示例:异步网络请求
假设我们使用 asio(Boost.Asio 或 standalone asio)来发起异步 HTTP 请求,下面给出一个简化示例:
#include <asio.hpp>
#include <iostream>
Task<std::string> async_http_get(asio::io_context& io, const std::string& host, const std::string& path) {
asio::ip::tcp::resolver resolver(io);
auto endpoints = co_await resolver.async_resolve(host, "http", asio::use_awaitable);
asio::ip::tcp::socket socket(io);
co_await asio::async_connect(socket, endpoints, asio::use_awaitable);
std::string request = "GET " + path + " HTTP/1.1\r\n"
"Host: " + host + "\r\n"
"Connection: close\r\n\r\n";
co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable);
std::string response;
asio::streambuf buffer;
for (;;) {
std::size_t n = co_await asio::async_read(socket, buffer, asio::transfer_at_least(1), asio::use_awaitable);
std::istream is(&buffer);
std::string chunk;
std::getline(is, chunk, '\0');
response += chunk;
if (n < 1) break; // 结束读取
}
co_return response;
}
三、协程的优势与注意事项
1. 优势
- 代码可读性:协程让异步代码呈现同步写法。
- 资源消耗低:协程内部仅保留必要状态,不需要系统线程堆栈。
- 组合灵活:可以组合
co_await、co_yield,实现流式处理。
2. 注意事项
- 异常处理:协程内部的异常会被
promise捕获,必须在await_resume()中重新抛出或处理。 - 生命周期:协程句柄持有
promise,需要确保对象在使用前未被销毁。 - 调试支持:IDE 可能不支持调试协程中的暂停点,需要使用日志或断点替代。
四、最佳实践
-
使用标准库协程包装
C++20 标准提供std::generator、std::future的协程版本,尽量使用官方实现,避免手写 Promise 逻辑。 -
尽量避免阻塞
在协程中不要使用std::this_thread::sleep_for等阻塞调用,改为co_await形式的异步等待。 -
错误恢复
通过try/catch包裹协程逻辑,并使用co_return返回错误码或错误对象。 -
资源释放
确保协程结束时资源被释放,利用 RAII 与协程内co_await结合实现。
五、结语
C++20 的协程为异步编程带来了革命性的改变,让代码更简洁、更易维护。掌握协程的基本语法、awaitable 对象的实现以及与第三方库的配合,是现代 C++ 开发者不可或缺的技能。希望本文能帮助你在项目中快速落地协程,开启更高效、可读的异步代码新时代。