协程(coroutine)是 C++20 标准引入的一项重要特性,它使得异步编程与生成器模式可以用更直观、可维护的语法来实现。本文将从协程的核心概念出发,介绍其实现机制、关键标准库组件,并给出一个完整的协程使用示例,帮助读者快速上手。
一、协程的基本概念
-
协程 与 线程 的区别
- 线程是操作系统调度的独立执行单元,协程是语言层面提供的轻量级“子程序”,在同一线程内部切换执行。
- 协程通过
co_await、co_yield、co_return关键词实现挂起与恢复,线程则通过std::thread、std::async等方式创建。
-
挂起点(suspend point)
co_await、co_yield、co_return是协程的挂起点。- 当协程在这些点被挂起时,调用方可以决定何时恢复执行。
-
协程句柄(coroutine handle)
- 通过
std::coroutine_handle<>获取协程的句柄,从句柄可以resume()、destroy()或查询done()状态。
- 通过
二、协程的实现细节
C++20 协程的实现依赖于三大核心组成:
-
promise_type
- 协程的“承诺”对象,用来存放协程的状态、返回值等。
- 必须实现若干成员函数,如
get_return_object(),initial_suspend(),final_suspend(),return_value()等。
-
awaiter
- 由
co_await后面的表达式生成。 - 需要实现
await_ready(),await_suspend(),await_resume()三个函数。 await_ready()判断是否需要挂起;await_suspend()把协程句柄传给awaiter,决定挂起行为;await_resume()在恢复时返回值。
- 由
-
协程框架
std::coroutine_handle<>与std::suspend_always/std::suspend_never用来控制挂起行为。- `std::generator `(实验性)提供了生成器接口。
三、关键标准库组件
| 组件 | 说明 |
|---|---|
std::suspend_always |
每次挂起 |
std::suspend_never |
永不挂起 |
| `std::coroutine_handle | |
| 句柄类型,T为promise_type` |
|
| `std::generator | |
| ` | 生成器,简化协程的使用(C++20 之后的标准提案) |
std::async |
支持协程的 std::future 版本 |
| `std::task | |
| 代表异步任务,返回std::future` |
四、协程完整示例:异步下载与流式处理
下面演示如何使用协程实现一个简单的“异步下载器”,在下载过程中实时输出进度,并在下载完成后返回文件内容。
#include <coroutine>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <vector>
#include <exception>
#include <filesystem>
using namespace std::chrono_literals;
// 简化的异步等待器,模拟网络延迟
struct async_sleep {
std::chrono::milliseconds dur;
async_sleep(std::chrono::milliseconds d) : dur(d) {}
bool await_ready() noexcept { return false; } // 总是挂起
// 挂起时,启动后台线程等待
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, this]() {
std::this_thread::sleep_for(dur);
h.resume(); // 延迟后恢复协程
}).detach();
}
void await_resume() noexcept {} // 恢复后不返回值
};
// 协程 promise_type
struct downloader_promise {
std::string result; // 下载结果
// 协程入口
std::suspend_always initial_suspend() noexcept { return {}; }
// 协程退出
std::suspend_always final_suspend() noexcept { return {}; }
// 生成协程句柄
struct coroutine_handle {
std::coroutine_handle <downloader_promise> h;
std::string operator()() {
h.destroy(); // 释放资源
return h.promise().result;
}
};
coroutine_handle get_return_object() noexcept {
return coroutine_handle{std::coroutine_handle <downloader_promise>::from_promise(*this)};
}
// 异常处理
void unhandled_exception() {
std::terminate();
}
// 返回值
void return_value(const std::string& v) {
result = v;
}
};
// 协程返回类型
using downloader_task = std::coroutine_handle <downloader_promise>;
// 模拟下载过程的协程
downloader_task download_file(const std::string& url, std::function<void(double)> progress_callback) {
const int total_parts = 10;
std::string data;
for (int i = 1; i <= total_parts; ++i) {
co_await async_sleep(200ms); // 模拟网络延迟
data += "chunk" + std::to_string(i); // 模拟下载的数据块
progress_callback(static_cast <double>(i) / total_parts * 100.0);
}
co_return data; // 返回完整数据
}
int main() {
std::cout << "开始下载...\n";
auto task = download_file("http://example.com/file",
[](double percent){ std::cout << "已下载: " << percent << "%\n"; });
// 手动驱动协程直到完成
while (!task.done()) {
task.resume();
}
std::string content = task(); // 通过句柄获取返回值
std::cout << "下载完成,内容长度:" << content.size() << "\n";
return 0;
}
代码说明
async_sleep:自定义 awaiter,用来模拟异步延迟。downloader_promise:协程的 promise_type,负责管理返回值和异常。download_file:协程主体,使用co_await挂起模拟网络延迟,使用co_return返回下载结果。main:手动轮询协程句柄直到完成,随后获取返回值。
五、协程的优势与局限
| 优势 | 说明 |
|---|---|
| 轻量级 | 协程不需要系统线程,切换成本低 |
| 可读性 | 代码写法接近同步风格,逻辑清晰 |
| 组合性 | co_await 允许组合多个异步操作 |
| 局限 | 说明 |
|---|---|
| 调试工具支持不足 | 断点与调用栈追踪仍在完善中 |
| 标准库支持仍在发展 | 某些工具(如 std::generator)还为实验性 |
| 线程安全 | 协程在单线程内切换,跨线程协作需自行设计 |
六、结语
C++20 的协程特性为异步编程提供了强有力且自然的语法支持。通过理解 promise_type、awaiter 与协程句柄的工作机制,结合标准库的辅助组件,开发者可以写出更简洁、更易维护的异步代码。希望本文的概念梳理与示例能帮助你快速上手并在项目中发挥协程的价值。