C++20 新增的协程(coroutine)特性,为异步编程提供了更简洁、更高效的语法。相比传统的回调或 promise‑future 方式,协程以同步的写法实现非阻塞逻辑,极大提升代码可读性。本文将从协程的基本概念、关键字使用,到实际案例——实现一个异步数据流消费,逐步带你掌握协程的核心。
一、协程的基本概念
协程是“可以暂停和恢复执行的函数”。在 C++ 中,协程由 co_await、co_yield、co_return 三个关键字实现,并且需要一个 协程返回类型(如 std::future、std::generator 或自定义类型)。协程在执行到 co_await 时会挂起,等待外部事件完成后恢复执行;co_yield 用于生成值流,调用方通过迭代获取每个生成值。
二、关键字解析
| 关键字 | 用途 | 说明 |
|---|---|---|
co_await |
暂停协程等待某个可等待对象完成 | 只适用于返回类型为 std::future、std::task 等 |
co_yield |
产生一个值,暂停协程 | 常用于实现生成器(generator) |
co_return |
结束协程并返回结果 | 生成最终返回值或结束信号 |
三、协程返回类型
C++20 并未提供统一的协程返回类型,而是留给实现者自定义。最常见的两种:
- **`std::future `**:标准库提供的异步结果容器。协程可以返回 `std::future`,调用方通过 `get()` 获取结果。
- **`std::generator `**(或自定义 `generator`):用于生成值流,类似 Python 的 `yield`。
备注:
std::generator在标准库中并未直接定义,但在很多实现(如 libstdc++、libc++)中可用。若不想依赖实现,可自定义一个简单的生成器。
四、实战示例:异步文件读取 + 数据流消费
下面演示一个完整的协程应用:读取一个文本文件,逐行解析后生成关键字列表,并异步处理每个关键字。
1. 工具类:异步文件读取
#include <fstream>
#include <string>
#include <future>
struct async_file_reader {
std::ifstream in;
async_file_reader(const std::string& path) : in(path) {}
// 返回一个 future,表示一次读取行操作
std::future<std::string> read_line() {
return std::async(std::launch::async, [this] {
std::string line;
if (std::getline(in, line))
return line;
else
return std::string(); // 空串表示 EOF
});
}
};
2. 协程生成器:逐行读取
#include <coroutine>
#include <exception>
template<typename T>
struct generator {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T current_value;
std::exception_ptr exc;
generator get_return_object() {
return generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { exc = std::current_exception(); }
};
handle_type coro;
generator(handle_type h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
generator(generator&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
generator& operator=(generator&& other) noexcept {
if (this != &other) {
if (coro) coro.destroy();
coro = other.coro;
other.coro = nullptr;
}
return *this;
}
class iterator {
handle_type coro;
public:
explicit iterator(handle_type h) : coro(h) {
if (coro) coro.resume();
}
iterator& operator++() {
coro.resume();
return *this;
}
const T& operator*() const { return coro.promise().current_value; }
const T* operator->() const { return &coro.promise().current_value; }
bool operator==(std::default_sentinel_t) const { return !coro || coro.done(); }
};
iterator begin() { return iterator{coro}; }
std::default_sentinel_t end() { return {}; }
};
3. 协程实现:读取并生成关键字
generator<std::string> line_generator(const std::string& path) {
async_file_reader reader(path);
while (true) {
auto fut = reader.read_line();
std::string line = fut.get(); // 这里为了示例同步等待,可改为 co_await
if (line.empty()) break;
co_yield line; // 暂停协程并返回当前行
}
}
注意:在真实异步场景中,应使用
co_await fut而非fut.get(),以避免阻塞线程。
4. 消费器:并行处理关键字
void process_keywords(const std::string& file_path) {
for (auto&& line : line_generator(file_path)) {
// 简单示例:将每行拆分为单词
std::istringstream iss(line);
std::string word;
while (iss >> word) {
// 这里模拟异步处理(例如网络请求)
std::async(std::launch::async, [word] {
std::cout << "Processing keyword: " << word << std::endl;
});
}
}
}
5. 主函数
int main() {
const std::string file = "sample.txt";
process_keywords(file);
// 给异步任务留足时间完成(真实项目应更优雅的同步)
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
五、性能与注意事项
- 协程内存占用:协程需要维护状态机,编译器会生成堆栈帧。避免在协程中大对象直接存放,可使用
co_yield std::move(obj)或 `co_yield std::make_shared ()`。 - 异常传播:协程的
promise_type可捕获异常,co_return会抛出异常给调用方。 - 调试难度:协程在调试时可能导致栈跟踪失真,建议在调试时使用
-g并结合lldb或gdb的协程支持。
六、结语
C++20 的协程为异步编程提供了极具表达力的工具。通过理解 co_await、co_yield 与自定义协程返回类型,你可以在保持同步代码可读性的同时实现高性能的异步逻辑。未来的标准(C++23、C++26)预计会进一步完善协程生态,加入更丰富的可等待对象、标准库容器支持等。保持关注,掌握协程,你的 C++ 项目将更具竞争力。