C++20 的协程(coroutine)是一项强大的语言特性,它让异步编程变得像同步编程一样直观。协程通过语言级别的支持实现了暂停、恢复和返回值的概念,极大简化了基于事件驱动、网络 IO、UI 更新等场景的代码。本文将从协程的核心概念、实现原理以及一个实用的文件读取示例三方面,系统阐述 C++20 协程的实用价值。
1. 协程的核心概念
-
协程句柄(
std::coroutine_handle)
协程句柄是协程与外部世界交互的桥梁。它可以用来挂起、恢复或销毁协程。句柄内部持有协程状态机的入口地址与上下文信息。 -
协程返回类型(
std::suspend_always/std::suspend_never)
这两个类型告诉编译器协程在何时挂起。suspend_always在每一次co_await处暂停,suspend_never则不暂停。 -
co_await、co_yield与co_returnco_await用于等待一个可等待对象,编译器会把它拆分成await_ready,await_suspend,await_resume三个阶段。co_yield用于生成值,适用于实现生成器。co_return用于返回协程最终值并结束协程。
2. 协程的实现原理
C++ 协程的实现可视为一个隐式的状态机。编译器会把协程函数拆分成若干状态块,并在 co_await、co_yield 处生成跳转点。
- 栈展开:协程的局部变量在堆上分配,避免了堆栈不够时的栈溢出问题。
- Promise 对象:每个协程都有一个
promise_type,用于存放协程返回值、异常信息以及状态机的入口。编译器在协程进入和退出时自动调用get_return_object,initial_suspend,final_suspend,return_value,unhandled_exception等函数。 - 协程帧(Coroutine Frame):是一块在堆上分配的内存块,存放协程的栈帧、promise 对象以及其他必要信息。
3. 典型使用场景
- 异步 IO:利用
co_await等待事件完成,避免回调地狱。 - 生成器:通过
co_yield实现惰性序列。 - 协程调度器:结合事件循环,实现协程切换与调度。
4. 实战示例:异步读取文件
下面给出一个使用 C++20 协程实现异步文件读取的完整示例。该示例演示了如何将标准文件 IO 适配为可等待对象,并在主程序中使用 co_await 简洁地读取文件。
#include <iostream>
#include <coroutine>
#include <string>
#include <vector>
#include <thread>
#include <chrono>
#include <fstream>
#include <sstream>
#include <mutex>
#include <condition_variable>
// ---------- 1. 可等待对象 AsyncFile ----------
struct AsyncFile {
struct promise_type {
std::string data;
std::exception_ptr ex;
std::condition_variable cv;
std::mutex mtx;
bool ready{false};
AsyncFile get_return_object() {
return AsyncFile{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { ex = std::current_exception(); }
template<class T>
void return_value(T&& value) { data = std::forward <T>(value); }
};
std::coroutine_handle <promise_type> coro;
AsyncFile(std::coroutine_handle <promise_type> h) : coro(h) {}
~AsyncFile() { if (coro) coro.destroy(); }
// 让调用者等待文件读取完成
std::string await_resume() {
std::unique_lock<std::mutex> lk(coro.promise().mtx);
coro.promise().cv.wait(lk, [&] { return coro.promise().ready; });
if (coro.promise().ex) std::rethrow_exception(coro.promise().ex);
return coro.promise().data;
}
};
AsyncFile read_file_async(const std::string& path) {
try {
// 模拟耗时 IO
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::ifstream ifs(path, std::ios::binary);
if (!ifs) throw std::runtime_error("文件打开失败");
std::stringstream buffer;
buffer << ifs.rdbuf();
// 通知等待者
auto& prom = std::coroutine_handle<AsyncFile::promise_type>::from_promise(*ifs);
prom.promise().ready = true;
prom.promise().cv.notify_all();
co_return buffer.str();
} catch (...) {
std::coroutine_handle<AsyncFile::promise_type>::from_promise(*ifs).promise().ex = std::current_exception();
co_return std::string{};
}
}
// ---------- 2. 主协程 ----------
struct MainTask {
struct promise_type {
std::coroutine_handle <promise_type> coro;
MainTask get_return_object() { return {coro}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
MainTask(std::coroutine_handle <promise_type> h) : coro(h) {}
~MainTask() { if (coro) coro.destroy(); }
};
MainTask main_task() {
std::cout << "开始读取文件...\n";
std::string content = co_await read_file_async("example.txt");
std::cout << "文件内容已读取,长度为:" << content.size() << " 字节\n";
}
int main() {
auto task = main_task();
// 这里简单等待任务结束
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
代码说明
- AsyncFile:包装了协程的 promise,内部通过条件变量与
std::mutex来同步读取完成。 - read_file_async:示例中的异步读取逻辑使用
std::this_thread::sleep_for模拟 IO 延迟。真实项目中可以使用 ASIO、WinIO 等库实现真正的异步 IO。 - main_task:演示了如何在主协程中使用
co_await等待文件读取完成,并处理返回值。
5. 小结
C++20 协程通过语言层面的支持,彻底改变了传统同步/异步编程的面貌。它不需要额外的回调或状态机手写,能够让复杂的异步流程写成像同步一样直观的代码。
- 优势:代码简洁、易维护、性能可控。
- 局限:需要编译器支持(至少 C++20)并可能导致堆分配开销。
- 未来:随着协程调度器、事件循环库的成熟,C++ 的异步编程将进一步完善。
希望本文能帮助你快速上手 C++20 协程,并在项目中发挥其强大优势。