掌握 C++20 里的协程:从理论到实践

在 C++20 标准发布后,协程(Coroutines)成为了语言的核心特性之一。它们允许程序在执行过程中挂起、恢复甚至并发地执行多个任务,而无需手动管理线程或状态机。本文将从协程的基本概念开始,逐步介绍如何在 C++ 项目中使用协程实现异步 I/O、流式数据处理和并发任务调度,并讨论常见的陷阱与最佳实践。

1. 协程基础概念

协程是一种可以在执行时暂停并恢复的函数。它通过 co_awaitco_yieldco_return 关键字与编译器交互,生成一个状态机。编译器会把协程的执行状态保存在一个 promise_type 对象中,随后在需要时恢复。

  • co_await:等待一个可等待对象完成,然后继续执行。
  • co_yield:产生一个值并暂停协程,类似生成器。
  • co_return:结束协程并返回最终值。

2. 编写一个简单的协程

#include <coroutine>
#include <iostream>
#include <string_view>

struct Task {
    struct promise_type {
        std::string value;
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(std::string_view str) { value = str; }
        void unhandled_exception() { std::terminate(); }
    };
};

Task hello_world() {
    std::cout << "Hello, ";
    co_await std::suspend_always{};
    std::cout << "World!\n";
    co_return "Done";
}

int main() {
    hello_world();
}

这段代码演示了一个最简协程:在 Hello,World! 之间暂停。实际开发中,协程更常用于 I/O 或生成流。

3. 协程与异步 I/O

C++20 标准并未直接提供异步 I/O,但可与 std::experimental::filesystemasio 等库结合使用。以 asio 为例:

#include <asio.hpp>
#include <coroutine>

asio::awaitable <void> async_read(asio::ip::tcp::socket& sock) {
    char buffer[1024];
    std::size_t n = co_await sock.async_read_some(
        asio::buffer(buffer), asio::use_awaitable);
    std::cout << "Received: " << std::string(buffer, n) << '\n';
}

这里 async_read 协程会在 I/O 完成前挂起,asio 内部会在 I/O 事件到来时恢复协程,极大简化了回调地狱。

4. 协程生成器(流式数据处理)

使用 co_yield 可以轻松实现生成器:

#include <coroutine>
#include <vector>

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 {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    struct iterator {
        using coro_handle = std::coroutine_handle <promise_type>;
        coro_handle handle;
        iterator(coro_handle h) : handle(h) { handle.resume(); }
        ~iterator() { if (handle) handle.destroy(); }

        iterator& operator++() {
            handle.resume();
            return *this;
        }

        T const& operator*() const { return handle.promise().current_value; }
        bool operator==(std::default_sentinel_t) const {
            return !handle || handle.done();
        }
    };

    using coro_handle = std::coroutine_handle <promise_type>;
    coro_handle coro;

    generator(coro_handle h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }
    auto begin() { return iterator(coro); }
    auto end() { return std::default_sentinel; }
};

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

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

输出:1 2 3 4。生成器模式使得按需计算和惰性迭代成为可能。

5. 并发任务调度

协程可以与线程池结合,实现高效并发。例如:

#include <asio.hpp>
#include <coroutine>

asio::thread_pool pool(4);

asio::awaitable <void> worker(int id) {
    for (int i = 0; i < 10; ++i) {
        std::cout << "Worker " << id << " step " << i << '\n';
        co_await asio::post(pool, asio::use_awaitable);
    }
}

int main() {
    asio::co_spawn(pool, worker(1), asio::detached);
    asio::co_spawn(pool, worker(2), asio::detached);
    pool.join();
}

这里 asio::post 用来将协程切换到线程池中的线程,co_spawn 用来启动协程。通过 awaitable 的机制,实现了线程与协程的无缝切换。

6. 常见陷阱与最佳实践

陷阱 解决方案
过度使用协程导致栈空间膨胀 只在需要挂起的地方使用 co_await,不要把所有函数都改成协程
promise_type 对象的析构顺序问题 明确资源管理,使用 std::optionalstd::unique_ptr 保存临时资源
协程返回值被忽略 使用 co_return 明确返回值,或者将结果包装为 std::future
多线程环境下协程数据竞争 使用 std::atomic 或线程安全容器,避免共享可变状态

7. 未来展望

C++23 在协程方面继续扩展,新增 std::ranges::subrange 与协程的结合、协程的异常处理机制等。开发者应关注标准更新,以充分利用协程带来的性能与简洁性。

8. 结语

协程为 C++ 提供了一种天然且高效的并发模型。通过 co_awaitco_yield 等关键字,程序员可以以同步代码的方式编写异步逻辑,显著提升代码可读性。只需了解协程的基础结构、编译器生成的状态机以及与 IO 框架的集成方式,即可在项目中快速落地协程,解决复杂的并发与异步场景。祝你编码愉快!

发表评论