C++20 的协程:从基本概念到实际应用

在 C++20 之后,协程(coroutine)成为语言中一项重要的新特性。它们为异步编程、生成器以及状态机等模式提供了更为直观、类型安全和高效的实现方式。本文从协程的核心概念、关键类型、使用方式、实际示例以及常见坑点进行系统阐述,帮助你快速掌握并在项目中应用协程。


1. 协程到底是什么?

协程是一种轻量级的可挂起函数,可以在执行过程中暂停(co_awaitco_yieldco_return)并在之后恢复。与传统线程相比,协程是单线程基于事件循环的异步执行单元,切换开销极低。

1.1 核心语法

关键字 用途 说明
co_await 暂停协程,等待某个异步操作完成 必须与 awaitable 对象配合使用
co_yield 暂停协程,产生一个值 用于生成器(generator)
co_return 结束协程并返回值 return 类似,但可在协程内部多次使用

1.2 awaitable、awaiter、promise

  • awaitable:协程所等待的对象(如 std::future, std::generator 等)。
  • awaiter:通过 await_ready, await_suspend, await_resume 三个成员函数定义等待逻辑。
  • promise:协程的承诺对象,用来存储协程的返回值、异常、状态等。

2. 协程的实现细节

协程本质上是由编译器把一个普通函数拆分成若干状态块,生成一个 state machine。编译器会生成两个关键结构:

  1. promise_type:定义协程返回类型、错误处理等。
  2. coroutine_handle:用于操作协程的句柄(挂起、恢复、销毁)。

编译器根据 co_awaitco_yieldco_return 的位置自动插入状态切换代码,无需手写状态机。


3. 典型协程用例

3.1 生成器(Generator)

#include <coroutine>
#include <iostream>

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 generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::exit(1); }
    };

    std::coroutine_handle <promise_type> coro;
    generator(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }

    bool next() { return coro.resume(); }
    T value() { return coro.promise().current_value; }
};

generator <int> range(int n) {
    for (int i = 0; i < n; ++i)
        co_yield i;
}

int main() {
    auto gen = range(5);
    while (gen.next())
        std::cout << gen.value() << ' ';
}

输出:

0 1 2 3 4 

3.2 异步 I/O 示例

假设我们有一个异步文件读取 async_read_file,返回 awaitable<std::string>

struct async_file_reader {
    std::string data;
    struct awaiter {
        async_file_reader* reader;
        bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) const {
            // 假设异步 I/O 在后台线程完成后调用 resume()
            std::thread([=]{
                std::this_thread::sleep_for(std::chrono::seconds(1));
                reader->data = "Hello, coroutine!";
                h.resume();
            }).detach();
        }
        std::string await_resume() const noexcept { return reader->data; }
    };
    awaiter operator co_await() { return {this}; }
};

async_file_reader async_read_file(const std::string& path) {
    // 在真正项目中,这里会发起异步文件读取请求
    async_file_reader reader;
    co_return reader;
}

async_task <void> demo() {
    auto reader = co_await async_read_file("sample.txt");
    std::cout << "File content: " << reader.data << '\n';
}

提示async_task 是用户自定义的 awaitable,用来包装协程入口。常见实现方式是 std::future 或第三方库(如 cppcoro::task)。


4. 常见坑点与最佳实践

序号 坑点 解决方案
1 未使用 co_await 语义的 awaitable await_ready() 必须返回 false,否则协程会立即完成,导致 co_await 失效。
2 资源泄漏 确保 coroutine_handle 在退出前 destroy(),或者使用 generator 的析构自动销毁。
3 异常传播 promise_type::unhandled_exception() 中手动转发或捕获。
4 跨线程挂起/恢复 co_awaitawait_suspend() 必须返回一个可复用的句柄。不要在线程中直接 resume() 句柄,除非保证线程安全。
5 性能瓶颈 避免在协程内部频繁创建临时对象,使用 std::move 或引用传递。
6 使用标准库 std::generator C++23 标准化 std::generator,可直接使用 `std::generator
` 而非自己实现。

5. 协程在项目中的落地

  1. 异步 I/O:将 asiolibuv 等库的异步接口包装为 awaitable,让业务代码像同步一样书写。
  2. 生成器:用于迭代大数据集、延迟序列或虚拟序列(如链表、树遍历)。
  3. 协程池:在高并发服务器中使用协程池管理协程生命周期,减少线程切换开销。
  4. 游戏循环:协程适合处理游戏事件、动画等时序任务,保持代码可读性。

6. 进一步学习资源

  • 《C++20 协程实战》
  • cppreference.com 对 std::generatorstd::future 的详细说明
  • “C++ Concurrency in Action” 之 “Coroutines” 章节
  • GitHub 上的 cppcoroasio 等协程实现库

结语

协程为 C++ 提供了一种既高效又表达力强的异步编程模型。掌握其基本语法、实现机制以及最佳实践后,你可以轻松编写可读、可维护且性能优异的异步代码。下一个步骤就是将协程融入你现有的项目,体会它带来的便利与性能提升吧。

发表评论