在 C++20 标准正式发布后,协程(coroutines)成为了提升异步编程体验的关键特性。与传统的回调、Promise 或 Future 方式相比,协程提供了更直观、更可读的代码结构。本文将从协程的基本概念、关键字、实现方式以及实际应用场景展开,帮助读者快速掌握 C++20 协程的使用技巧。
1. 协程的基本概念
协程是一种轻量级的子程序,可以在函数内部“挂起”并在之后继续执行。它与线程不同,协程的调度完全由程序自己控制,避免了线程切换的系统开销。C++20 的协程通过 co_await、co_yield、co_return 三个关键字实现挂起与恢复的逻辑。
co_await:等待一个 awaitable 对象完成。若对象不立即完成,协程会挂起,待对象完成后恢复执行。co_yield:生成一个值并挂起协程,类似于生成器函数。co_return:结束协outine 并返回最终值。
2. 必备的类型与接口
实现协程时,必须定义一个 promise type。promise type 用于管理协程的生命周期,包括挂起时保存状态、恢复时恢复状态以及最终返回值。C++20 标准库提供了 std::coroutine_handle、std::suspend_always、std::suspend_never 等工具。
struct task {
struct promise_type {
task get_return_object() {
return task{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
};
std::coroutine_handle <promise_type> handle;
task(std::coroutine_handle <promise_type> h) : handle(h) {}
~task() { if (handle) handle.destroy(); }
};
3. 示例:异步计数器
下面给出一个使用协程实现异步计数器的例子。该计数器每隔一秒输出一次数字,演示了 co_await 的使用。
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
struct sleep_awaiter {
std::chrono::milliseconds duration;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const noexcept {
std::thread([=]{
std::this_thread::sleep_for(duration);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
struct async_counter {
struct promise_type {
async_counter get_return_object() {
return async_counter{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
};
std::coroutine_handle <promise_type> h;
async_counter(std::coroutine_handle <promise_type> h) : h(h) {}
~async_counter() { if (h) h.destroy(); }
};
async_counter counter() {
for (int i = 1; i <= 5; ++i) {
std::cout << "Count: " << i << std::endl;
co_await sleep_awaiter{ std::chrono::milliseconds(1000) };
}
}
int main() {
auto c = counter();
while (!c.h.done()) std::this_thread::sleep_for(std::chrono::milliseconds(10));
return 0;
}
运行上述程序后,控制台会每秒输出一次计数,直到计数结束。
4. 与 std::future 的对比
传统的异步编程常使用 std::future 与 std::async:
auto fut = std::async(std::launch::async, []{ /* long task */ });
虽然易用,但 std::future 需要在后台线程中执行,无法在同一线程中实现异步流程控制。协程则可以在单线程环境下完成异步逻辑,避免了线程上下文切换成本。
5. 实际应用场景
- 网络编程:与
asio等库配合,协程可以编写非阻塞 I/O 代码,代码结构如同同步编程。 - 游戏循环:在游戏主循环中使用协程实现动画、物理模拟等任务的时间片切分。
- 生成器:使用
co_yield实现惰性序列生成,例如无限斐波那契数列。
6. 常见坑与建议
- 协程对象的生命周期:协程句柄在返回后不再拥有任何引用,需要显式管理或使用
std::shared_ptr包装。 - 异常传播:
promise_type::unhandled_exception必须处理异常,避免程序崩溃。 - 线程安全:协程自身不是线程安全的,若在多线程中共享,需要自行同步。
7. 结语
C++20 的协程为异步编程带来了革命性的改进,代码可读性和维护性大幅提升。虽然协程的语法相对复杂,但掌握后可以在多种领域编写高效、优雅的异步代码。建议在日常项目中尝试使用协程替代传统回调模式,逐步培养协程思维。祝你编码愉快!