在 C++20 中,协程(coroutines)被引入为一种语言级别的异步编程机制。相较于传统的线程或基于回调的异步模型,协程能够让开发者用同步代码的语法书写异步逻辑,从而提升可读性和维护性。本文将从协程的基础概念出发,演示如何在 C++20 中使用协程实现一个简单的异步任务调度器,并讨论常见的陷阱与最佳实践。
1. 协程的核心概念
co_await:挂起当前协程,等待一个可等待对象(awaitable)完成。co_yield:生成一个值并挂起协程,等待调用方继续。co_return:返回协程的最终结果并结束协程。- Awaitable:实现
await_ready()、await_suspend()、await_resume()的对象,决定挂起行为、恢复方式和返回值。
协程本质上是一种“暂停点”集合。每个暂停点都是一个状态机,编译器在编译期将其拆解成生成函数,并在运行时通过 promise_type 与调用者交互。
2. 一个最小的协程实现
下面给出一个最简的 async_task,它包装了一个可以被 co_await 的协程。
#include <coroutine>
#include <exception>
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
template<typename T>
struct async_task {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T value_;
std::exception_ptr exception_;
async_task get_return_object() {
return async_task{handle_type::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template<typename U>
void return_value(U&& val) { value_ = std::forward <U>(val); }
};
handle_type h_;
explicit async_task(handle_type h) : h_(h) {}
~async_task() { if (h_) h_.destroy(); }
// 让外部能够使用 awaitable 接口
auto operator co_await() const {
struct awaiter {
handle_type h_;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> caller) const noexcept {
// 这里简化为直接在线程中执行
std::thread([h=std::move(h_), caller](){
h.resume();
caller.resume();
}).detach();
}
T await_resume() {
if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_);
return h_.promise().value_;
}
};
return awaiter{h_};
}
};
async_task <int> async_add(int a, int b) {
std::cout << "Start async_add in thread " << std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
co_return a + b;
}
int main() {
auto task = async_add(3, 4);
int result = task.await_resume(); // 这里直接拿值,实际使用中会使用 co_await
std::cout << "Result: " << result << "\n";
}
注意:上述代码仅作演示,实际项目中需要更完善的异常处理与线程池管理。
3. 构建一个简单的任务调度器
在实际使用中,往往需要把多个协程调度到一个线程池中,以减少线程上下文切换。下面给出一个极简的调度器实现:
#include <coroutine>
#include <vector>
#include <queue>
#include <thread>
#include <condition_variable>
#include <atomic>
class TaskScheduler {
public:
using Task = std::coroutine_handle<std::suspend_always::promise_type>;
TaskScheduler(size_t thread_count = std::thread::hardware_concurrency())
: stop_(false)
{
for (size_t i = 0; i < thread_count; ++i)
workers_.emplace_back([this](){ this->worker_loop(); });
}
~TaskScheduler() {
stop_ = true;
cv_.notify_all();
for (auto& th : workers_) th.join();
}
void enqueue(Task t) {
{
std::lock_guard<std::mutex> lk(mtx_);
queue_.push(t);
}
cv_.notify_one();
}
private:
void worker_loop() {
while (!stop_) {
Task t;
{
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [&]{ return stop_ || !queue_.empty(); });
if (stop_ && queue_.empty()) return;
t = queue_.front();
queue_.pop();
}
t.resume();
}
}
std::vector<std::thread> workers_;
std::queue <Task> queue_;
std::mutex mtx_;
std::condition_variable cv_;
std::atomic <bool> stop_;
};
结合 async_task,你可以将协程包装为 TaskScheduler::Task 并提交:
TaskScheduler scheduler;
auto task = async_add(10, 20);
// 将协程包装为 awaitable,然后提交
scheduler.enqueue(task.get_coro_handle());
4. 常见陷阱与最佳实践
-
不要在协程内部直接调用
std::this_thread::sleep_for
这会阻塞当前线程,导致协程失去异步的意义。推荐使用异步定时器(如 Boost.Asio 的 steady_timer)来挂起协程。 -
避免在
await_suspend中捕获异常
如果在挂起期间抛出异常,协程的promise_type会记录该异常。务必在await_resume处正确恢复。 -
避免返回局部对象
协程返回的对象会在协程结束后被销毁,若使用引用或指针要确保生命周期安全。 -
使用
co_await std::suspend_always或suspend_never
根据需求决定是否在协程起始/结束处挂起,避免不必要的上下文切换。 -
线程安全的协程返回值
若协程结果需要在多线程间共享,建议使用std::promise/std::future之类的同步原语包装,或在协程内部自行同步。
5. 结语
C++20 的协程为异步编程提供了一种更直观、类型安全的手段。虽然在实现上仍需要对 promise_type、awaitable 以及调度机制有一定的理解,但通过正确的设计,协程可以大幅简化异步代码结构,提升可读性。希望本文的示例能帮助你快速上手 C++20 协程,并在实际项目中得到应用。