C++20 协程与异步编程的实践

在 C++20 标准正式发布后,协程(coroutines)成为了提升异步编程体验的关键特性。与传统的回调、Promise 或 Future 方式相比,协程提供了更直观、更可读的代码结构。本文将从协程的基本概念、关键字、实现方式以及实际应用场景展开,帮助读者快速掌握 C++20 协程的使用技巧。

1. 协程的基本概念

协程是一种轻量级的子程序,可以在函数内部“挂起”并在之后继续执行。它与线程不同,协程的调度完全由程序自己控制,避免了线程切换的系统开销。C++20 的协程通过 co_awaitco_yieldco_return 三个关键字实现挂起与恢复的逻辑。

  • co_await:等待一个 awaitable 对象完成。若对象不立即完成,协程会挂起,待对象完成后恢复执行。
  • co_yield:生成一个值并挂起协程,类似于生成器函数。
  • co_return:结束协outine 并返回最终值。

2. 必备的类型与接口

实现协程时,必须定义一个 promise type。promise type 用于管理协程的生命周期,包括挂起时保存状态、恢复时恢复状态以及最终返回值。C++20 标准库提供了 std::coroutine_handlestd::suspend_alwaysstd::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::futurestd::async

auto fut = std::async(std::launch::async, []{ /* long task */ });

虽然易用,但 std::future 需要在后台线程中执行,无法在同一线程中实现异步流程控制。协程则可以在单线程环境下完成异步逻辑,避免了线程上下文切换成本。

5. 实际应用场景

  1. 网络编程:与 asio 等库配合,协程可以编写非阻塞 I/O 代码,代码结构如同同步编程。
  2. 游戏循环:在游戏主循环中使用协程实现动画、物理模拟等任务的时间片切分。
  3. 生成器:使用 co_yield 实现惰性序列生成,例如无限斐波那契数列。

6. 常见坑与建议

  • 协程对象的生命周期:协程句柄在返回后不再拥有任何引用,需要显式管理或使用 std::shared_ptr 包装。
  • 异常传播promise_type::unhandled_exception 必须处理异常,避免程序崩溃。
  • 线程安全:协程自身不是线程安全的,若在多线程中共享,需要自行同步。

7. 结语

C++20 的协程为异步编程带来了革命性的改进,代码可读性和维护性大幅提升。虽然协程的语法相对复杂,但掌握后可以在多种领域编写高效、优雅的异步代码。建议在日常项目中尝试使用协程替代传统回调模式,逐步培养协程思维。祝你编码愉快!

发表评论