C++20 中的协程:从理论到实践

协程(coroutine)是 C++20 标准中新加入的强大语法特性,它为实现轻量级协作式并发、异步 I/O、生成器等场景提供了统一而简洁的语法。本文将从协程的基本概念、关键字、返回类型、实现方式以及一个简易的异步任务示例,逐步展开讲解,帮助你快速掌握协程的使用方法。

1. 协程基础

1.1 什么是协程

协程是一种函数级别的协作式多任务单元,它允许函数在执行过程中“挂起”并在稍后恢复执行,而不需要线程切换。与线程相比,协程的切换成本更低,能够更好地控制执行顺序。

1.2 关键字

  • co_await: 等待一个 awaitable 对象完成。类似于 await
  • co_yield: 产生一个值给调用者。用于实现生成器。
  • co_return: 返回协程的最终值。

1.3 协程的生命周期

  • 生成:调用协程函数后得到一个 promise_type 对象。
  • 执行:协程开始执行直到遇到 co_await/co_yield/co_return
  • 挂起:遇到 co_await/co_yield 时协程挂起,状态保存在 promise_type 中。
  • 恢复:外部事件(如异步 I/O 完成)触发协程恢复执行。

2. 协程返回类型

C++20 规定协程函数的返回类型必须满足 std::experimental::coroutine_handlepromise_type 约束。最常见的返回类型有:

  • `std::future `(标准库)
  • `std::experimental::generator `(实验性)
  • 自定义 `Task `,内部使用 `std::coroutine_handle` 管理协程状态。

2.1 自定义 Task 示例

template<typename T>
struct Task {
    struct promise_type {
        T value;
        std::exception_ptr exception;

        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 unhandled_exception() { exception = std::current_exception(); }
        template<typename U>
        void return_value(U&& v) { value = std::forward <U>(v); }
    };

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

    T get() {
        if (coro.promise().exception) std::rethrow_exception(coro.promise().exception);
        return coro.promise().value;
    }
};

3. 协程的 awaitable 对象

任何类型只要满足 operator co_await,或者提供 await_ready/await_suspend/await_resume 成员函数,即可作为 awaitable。常见的 awaitable:

  • `std::future `(可等待异步结果)
  • `std::experimental::generator `(可等待下一个生成值)
  • 自定义异步 I/O 对象,例如 `asio::awaitable `(Boost.Asio)

3.1 await_ready/await_suspend/await_resume

struct SimpleAwaitable {
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        // 例如注册回调,完成后调用 h.resume()
    }
    int await_resume() const noexcept { return 42; }
};

4. 典型使用场景

4.1 生成器

#include <experimental/generator>

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

4.2 异步 I/O

#include <asio.hpp>
#include <iostream>

asio::awaitable <void> async_read_file(const std::string& path) {
    asio::file_handle file{ co_await asio::this_coro::executor };
    // ...
    std::string data = co_await file.async_read_some(...);
    std::cout << "Read " << data.size() << " bytes\n";
}

4.3 简易网络服务器

asio::awaitable <void> session(tcp::socket sock) {
    try {
        for (;;) {
            std::array<char, 1024> buf;
            std::size_t n = co_await sock.async_read_some(asio::buffer(buf));
            if (n == 0) break;
            co_await sock.async_write_some(asio::buffer(buf, n));
        }
    } catch (...) { /* 处理异常 */ }
}

asio::awaitable <void> server(tcp::acceptor& acceptor) {
    for (;;) {
        tcp::socket sock{ co_await acceptor.async_accept() };
        asio::co_spawn(acceptor.get_executor(), session(std::move(sock)), asio::detached);
    }
}

5. 性能与注意事项

  1. 协程不是线程:挂起/恢复是轻量级的,但仍需注意同步与数据竞争。
  2. 内存分配:协程的 promise_type 通常在堆上分配,避免频繁 new/delete 可以使用协程池或自定义分配器。
  3. 异常传播:异常会保存在 promise_type 中,通过 co_returnget() 传播。
  4. 标准库支持:C++20 标准库中已提供 std::future/std::generator,但许多实际项目仍依赖第三方库(如 Boost.Asio、cppcoro)。

6. 结语

协程为 C++20 带来了更高层次的异步编程模型,既能保持代码可读性,又能减少回调地狱。掌握协程的核心概念、关键字以及常见的 awaitable 类型后,你可以在网络编程、游戏逻辑、数据流处理等领域大显身手。建议先从简单的生成器练手,再逐步尝试异步 I/O,最后在项目中逐步替换传统回调或线程模型,享受协程带来的优雅与高效。

发表评论