C++20中的协程:从基础到实战

协程(Coroutines)是C++20引入的一个强大特性,它让异步编程变得像同步编程一样自然和简洁。下面我们从协程的基本概念开始,逐步深入到实际使用场景,帮助你快速掌握并应用到项目中。

1. 协程是什么?

协程是一种比线程更轻量级的“伪线程”概念。它允许函数在执行过程中挂起(co_await)、恢复(co_yield)或终止,而不需要手动管理线程或状态机。协程内部维护一个状态机,用来记录函数的暂停点和局部变量的值。

2. 协程的核心关键词

关键词 含义 典型用法
co_await 挂起协程,等待一个可等待对象完成 co_await async_operation();
co_yield 生成一个值给调用者,暂停协程 co_yield 42;
co_return 结束协程并返回一个值 co_return 0;

3. 一个简单的协程示例

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

// 1. 定义一个可等待的类型
struct SleepAwaitable {
    std::chrono::milliseconds duration;
    bool await_ready() const noexcept { return duration.count() == 0; }
    void await_suspend(std::coroutine_handle<> h) const {
        std::thread([h, dur = duration]() {
            std::this_thread::sleep_for(dur);
            h.resume(); // 唤醒协程
        }).detach();
    }
    void await_resume() const noexcept {}
};

SleepAwaitable sleep_for(std::chrono::milliseconds ms) {
    return SleepAwaitable{ms};
}

// 2. 协程返回类型
struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

// 3. 协程函数
Task example_coroutine() {
    std::cout << "Start\n";
    co_await sleep_for(std::chrono::seconds(1));
    std::cout << "Middle after 1s\n";
    co_await sleep_for(std::chrono::seconds(2));
    std::cout << "End after 2s\n";
}

int main() {
    example_coroutine(); // 协程立即执行,挂起后继续
    std::this_thread::sleep_for(std::chrono::seconds(4)); // 主线程等待
}

运行结果:

Start
Middle after 1s
End after 2s

这个例子展示了如何用 co_await 挂起协程,等待一个自定义的 SleepAwaitable 完成。

4. 协程与 std::generator

C++20 标准库中提供了 std::generator,专门用于生成一系列值。它结合 co_yield 的语义,像 Python 的生成器一样简单。

#include <iostream>
#include <generator>

std::generator <int> range(int start, int end) {
    for (int i = start; i <= end; ++i)
        co_yield i;
}

int main() {
    for (auto v : range(1, 5))
        std::cout << v << ' ';
}

输出:

1 2 3 4 5 

5. 协程与异步 I/O

协程是实现异步 I/O 的理想选择。典型框架(如 Boost.Asio、cpprestsdk、Pika)都在内部使用协程来隐藏事件循环的细节。下面给出一个伪代码示例,演示如何用协程包装异步文件读取。

struct AsyncRead {
    std::filesystem::path file;
    struct promise_type { /* 同上 */ };
    // ...
};

AsyncRead async_read_file(const std::string& path) {
    // 这里使用操作系统提供的异步 API
    co_await // ...
    // 读取完成后返回数据
    co_return std::vector <char>{...};
}

Task main_task() {
    auto data = co_await async_read_file("data.txt");
    std::cout << "Read " << data.size() << " bytes\n";
}

6. 性能与坑

  • 栈占用:协程的栈在编译时展开为状态机,局部变量会保存在堆栈上,但并不是线程栈,大小取决于你在协程里声明的变量。避免在协程里使用过大的数组。
  • 异常处理:协程内部异常会调用 unhandled_exception。你可以在 promise_type 中提供自定义异常捕获逻辑。
  • 与线程混用:协程本身不涉及线程切换,但如果你在协程里使用 std::thread 或者阻塞操作,仍然会产生线程上下文切换。建议把耗时操作封装成 co_await 的异步接口。

7. 小结

  • C++20 的协程让异步代码更加直观,几乎没有语法负担。
  • co_await 用于等待异步操作;co_yield 用于生成序列;co_return 用于返回结果。
  • 通过自定义 awaitable,可以将任何异步 API(网络、文件、定时器)变成协程友好。
  • std::generator 为生成器提供了标准实现。

掌握协程后,你可以轻松构建高性能的网络服务器、游戏引擎异步任务系统,甚至实现自己的协程框架。祝你玩得开心,编码愉快!

发表评论