在 C++20 标准发布后,协程(Coroutines)成为了语言的核心特性之一。它们允许程序在执行过程中挂起、恢复甚至并发地执行多个任务,而无需手动管理线程或状态机。本文将从协程的基本概念开始,逐步介绍如何在 C++ 项目中使用协程实现异步 I/O、流式数据处理和并发任务调度,并讨论常见的陷阱与最佳实践。
1. 协程基础概念
协程是一种可以在执行时暂停并恢复的函数。它通过 co_await、co_yield 和 co_return 关键字与编译器交互,生成一个状态机。编译器会把协程的执行状态保存在一个 promise_type 对象中,随后在需要时恢复。
co_await:等待一个可等待对象完成,然后继续执行。co_yield:产生一个值并暂停协程,类似生成器。co_return:结束协程并返回最终值。
2. 编写一个简单的协程
#include <coroutine>
#include <iostream>
#include <string_view>
struct Task {
struct promise_type {
std::string value;
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(std::string_view str) { value = str; }
void unhandled_exception() { std::terminate(); }
};
};
Task hello_world() {
std::cout << "Hello, ";
co_await std::suspend_always{};
std::cout << "World!\n";
co_return "Done";
}
int main() {
hello_world();
}
这段代码演示了一个最简协程:在 Hello, 与 World! 之间暂停。实际开发中,协程更常用于 I/O 或生成流。
3. 协程与异步 I/O
C++20 标准并未直接提供异步 I/O,但可与 std::experimental::filesystem、asio 等库结合使用。以 asio 为例:
#include <asio.hpp>
#include <coroutine>
asio::awaitable <void> async_read(asio::ip::tcp::socket& sock) {
char buffer[1024];
std::size_t n = co_await sock.async_read_some(
asio::buffer(buffer), asio::use_awaitable);
std::cout << "Received: " << std::string(buffer, n) << '\n';
}
这里 async_read 协程会在 I/O 完成前挂起,asio 内部会在 I/O 事件到来时恢复协程,极大简化了回调地狱。
4. 协程生成器(流式数据处理)
使用 co_yield 可以轻松实现生成器:
#include <coroutine>
#include <vector>
template<typename T>
struct generator {
struct promise_type {
T current_value;
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
generator get_return_object() { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
struct iterator {
using coro_handle = std::coroutine_handle <promise_type>;
coro_handle handle;
iterator(coro_handle h) : handle(h) { handle.resume(); }
~iterator() { if (handle) handle.destroy(); }
iterator& operator++() {
handle.resume();
return *this;
}
T const& operator*() const { return handle.promise().current_value; }
bool operator==(std::default_sentinel_t) const {
return !handle || handle.done();
}
};
using coro_handle = std::coroutine_handle <promise_type>;
coro_handle coro;
generator(coro_handle h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
auto begin() { return iterator(coro); }
auto end() { return std::default_sentinel; }
};
generator <int> range(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
int main() {
for (int v : range(1, 5))
std::cout << v << ' ';
}
输出:1 2 3 4。生成器模式使得按需计算和惰性迭代成为可能。
5. 并发任务调度
协程可以与线程池结合,实现高效并发。例如:
#include <asio.hpp>
#include <coroutine>
asio::thread_pool pool(4);
asio::awaitable <void> worker(int id) {
for (int i = 0; i < 10; ++i) {
std::cout << "Worker " << id << " step " << i << '\n';
co_await asio::post(pool, asio::use_awaitable);
}
}
int main() {
asio::co_spawn(pool, worker(1), asio::detached);
asio::co_spawn(pool, worker(2), asio::detached);
pool.join();
}
这里 asio::post 用来将协程切换到线程池中的线程,co_spawn 用来启动协程。通过 awaitable 的机制,实现了线程与协程的无缝切换。
6. 常见陷阱与最佳实践
| 陷阱 | 解决方案 |
|---|---|
| 过度使用协程导致栈空间膨胀 | 只在需要挂起的地方使用 co_await,不要把所有函数都改成协程 |
| promise_type 对象的析构顺序问题 | 明确资源管理,使用 std::optional 或 std::unique_ptr 保存临时资源 |
| 协程返回值被忽略 | 使用 co_return 明确返回值,或者将结果包装为 std::future |
| 多线程环境下协程数据竞争 | 使用 std::atomic 或线程安全容器,避免共享可变状态 |
7. 未来展望
C++23 在协程方面继续扩展,新增 std::ranges::subrange 与协程的结合、协程的异常处理机制等。开发者应关注标准更新,以充分利用协程带来的性能与简洁性。
8. 结语
协程为 C++ 提供了一种天然且高效的并发模型。通过 co_await、co_yield 等关键字,程序员可以以同步代码的方式编写异步逻辑,显著提升代码可读性。只需了解协程的基础结构、编译器生成的状态机以及与 IO 框架的集成方式,即可在项目中快速落地协程,解决复杂的并发与异步场景。祝你编码愉快!