在 C++ 20 版本中,协程(coroutines)被正式纳入标准库,提供了一套完整而高效的异步编程模型。相比传统的回调或线程池,协程让异步代码更易读、易维护。本文将介绍协程的基本概念、关键类、常见使用场景,并提供一份可直接运行的示例代码,帮助你快速上手。
一、协程的基本概念
协程是一种轻量级的函数,它在执行过程中可以被暂停(co_await 或 co_yield),随后在需要时恢复。协程内部的状态会被保存在协程帧(coroutine frame)中,编译器会为协程生成一套隐藏的状态机。
1.1 关键关键字
co_await:等待一个 awaitable 对象完成,并在完成后恢复协程。co_yield:将一个值返回给调用者,挂起协程,等待下一次调用。co_return:返回一个最终值,结束协程。
1.2 awaitable 与 awaiter
协程需要等待的对象必须满足 awaitable 接口。最常见的是 std::future、std::shared_future、std::promise 或自定义的 awaitable。awaitable 会产生一个 awaiter 对象,awaiter 必须提供 await_ready()、await_suspend()、await_resume() 三个成员。
二、标准库中的协程工具
C++ 20 标准库提供了一些协程相关的模板,简化了常见需求。
| 模板 | 作用 | 典型使用场景 |
|---|---|---|
| `std::generator | ||
| 用于实现可迭代的协程,支持co_yield` |
生成序列、懒加载 | |
| `std::task | ||
| 用于异步任务,支持co_return` |
IO、网络请求 | |
std::suspend_always / std::suspend_never |
简单的挂起策略 | 基础控制 |
std::experimental::generator |
早期实现,兼容旧编译器 | 兼容性 |
三、协程的实践示例
下面给出一个完整的协程实现示例:一个异步下载器,使用 std::task<std::string> 下载网页内容,并在下载完成后打印。我们通过自定义 simple_http_get 来模拟异步 HTTP 请求。
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <future>
#include <coroutine>
#include <experimental/generator>
#include <optional>
// 简单的 awaitable:模拟异步 HTTP GET
struct async_http_get {
std::string url;
std::optional<std::string> result;
struct awaiter {
async_http_get& self;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
std::thread([this, h]() {
// 模拟网络延迟
std::this_thread::sleep_for(std::chrono::seconds(2));
self.result = "Content from " + self.url;
h.resume(); // 继续执行协程
}).detach();
}
std::string await_resume() const noexcept { return *self.result; }
};
awaiter operator co_await() { return { *this }; }
};
// 异步任务返回字符串
template<typename T>
struct task {
struct promise_type {
T value_;
std::exception_ptr eptr_;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T val) { value_ = std::move(val); }
void unhandled_exception() { eptr_ = std::current_exception(); }
task get_return_object() {
return task{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
};
std::coroutine_handle <promise_type> coro_;
task(std::coroutine_handle <promise_type> h) : coro_(h) {}
task(task&& other) noexcept : coro_(std::exchange(other.coro_, {})) {}
~task() { if (coro_) coro_.destroy(); }
std::future <T> result() {
return std::async(std::launch::async, [this]() {
if (coro_.promise().eptr_) std::rethrow_exception(coro_.promise().eptr_);
return coro_.promise().value_;
});
}
};
// 协程函数:异步下载
task<std::string> download(const std::string& url) {
async_http_get req{ url };
std::string body = co_await req; // 等待下载完成
co_return body;
}
int main() {
auto fut = download("https://example.com").result();
std::cout << "Download started...\n";
std::cout << "Result: " << fut.get() << stdn::endl;
return 0;
}
运行结果示例
Download started...
Result: Content from https://example.com
该示例演示了:
- 自定义 awaitable (
async_http_get) 并实现 awaiter 接口。 - 定义通用 `task ` 模板,包装协程返回值与异常。
- 在主函数中启动协程并通过
std::future方式等待结果。
四、协程性能与最佳实践
- 减少栈帧大小:协程帧会在堆上分配,使用
co_yield产生大量值时可考虑std::generator,减少复制成本。 - 错误传播:在
promise_type中使用unhandled_exception()捕获异常,并通过std::future传播。 - 线程安全:若协程跨线程,使用
std::atomic或互斥锁保护共享状态。 - 使用第三方库:如
cppcoro、folly或libuv的协程实现,提供更丰富的 IO、网络、线程池支持。
五、结语
C++ 20 的协程为异步编程带来了更接近同步代码的可读性和维护性。掌握 awaitable 接口、标准库协程工具以及正确的错误处理策略,你就能在 C++ 项目中快速构建高效、可扩展的异步功能。欢迎在评论区分享你使用协程的经验或遇到的坑!