**C++20 协程(Coroutines):从理论到实践的完整指南**

在 C++20 中,协程(Coroutines)被正式纳入标准库,为异步编程、生成器以及复杂的状态机实现提供了强大的语法支持。本文将从协程的基本概念、实现机制,到如何在实际项目中使用协程处理异步 I/O,提供一个完整的学习路径。


1. 协程的基本概念

协程是一种比传统线程更轻量级的并发模型。它可以在执行过程中被挂起(co_await/co_yield/co_return),随后在需要时恢复执行。与线程相比,协程不需要切换堆栈,减少了系统资源消耗和上下文切换开销。

关键术语

术语 定义
挂起点 co_await, co_yield, co_return 的位置
协程句柄 (std::coroutine_handle) 用于控制协程生命周期的对象
协程 promise 与协程状态关联的结构体,提供 get_return_object() 等成员
悬挂/恢复 协程被挂起后保持挂起状态,等待恢复

2. 协程的实现原理

协程本质上是编译器生成的状态机。co_await 之后的代码会被拆分为多个“分支”,每个分支对应一个生成器的状态。编译器将协程函数展开成一个 promise_type 结构体,内部保存所有需要保存的局部变量以及控制流状态。

代码示例

#include <coroutine>
#include <iostream>

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(); }
    };
};

Task myCoroutine() {
    std::cout << "Step 1\n";
    co_await std::suspend_always{}; // 挂起点
    std::cout << "Step 2\n";
    co_return;
}

编译后,myCoroutine 变成了一个状态机,co_await 处会生成一个暂停点,后续代码会被包装进 resume 方法中。


3. 典型使用场景

3.1 异步 I/O

协程最常见的用途是包装异步 I/O,例如文件、网络请求。使用 co_await 可以让代码保持同步的写法,同时隐藏异步等待的复杂性。

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>

boost::asio::awaitable <void> asyncRead(
    boost::asio::ip::tcp::socket& socket,
    std::vector <char>& buffer) {

    std::size_t n = co_await socket.async_read_some(
        boost::asio::buffer(buffer),
        boost::asio::use_awaitable);
    std::cout << "Read " << n << " bytes\n";
}

3.2 生成器

协程可以用来实现惰性序列,例如斐波那契数列、素数生成器。

#include <coroutine>
#include <iostream>
#include <vector>

template<typename T>
struct Generator {
    struct promise_type {
        T value_;
        std::suspend_always yield_value(T v) {
            value_ = v; 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::terminate(); }
    };

    std::coroutine_handle <promise_type> handle_;
    Generator(std::coroutine_handle <promise_type> h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }
    bool next() {
        if (!handle_.done()) handle_.resume();
        return !handle_.done();
    }
    T value() const { return handle_.promise().value_; }
};

Generator <int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int c = a + b;
        a = b; b = c;
    }
}

int main() {
    auto gen = fibonacci();
    for (int i = 0; i < 10 && gen.next(); ++i)
        std::cout << gen.value() << " ";
}

3.3 状态机

协程可以简化复杂状态机的实现,例如游戏 AI 或 UI 事件流。


4. 性能与资源管理

协程的优势在于轻量级,但仍需注意以下几点:

  1. 内存占用:协程体内所有局部变量会被拆分为成员,导致内存占用不如普通函数。可通过 co_yield 只存储必要变量。
  2. 异常传播:在 promise_type 中实现 unhandled_exception() 可以捕获异常并决定处理方式。若未处理会 std::terminate()
  3. 调度器:协程本身不包含调度逻辑,需要配合事件循环(如 Boost.Asio、libuv)或自定义调度器。

5. 实际项目案例

5.1 网络服务端

使用 Boost.Asio + C++20 协程实现一个简单的 HTTP 服务器。

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>

using namespace boost::asio;

awaitable <void> handleConnection(tcp::socket socket) {
    char buffer[1024];
    std::size_t n = co_await socket.async_read_some(buffer(buffer), use_awaitable);
    std::string request(buffer, n);
    std::cout << "Received request: " << request << "\n";

    std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world";
    co_await async_write(socket, buffer(response), use_awaitable);
}

awaitable <void> server(uint16_t port) {
    io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), port));
    for (;;) {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
        co_spawn(io, handleConnection(std::move(socket)), detached);
    }
}

5.2 数据流处理

使用协程来实现一个数据流管道,支持异步读取、处理和写入。

awaitable <void> dataPipeline() {
    std::vector <char> buffer(4096);

    while (true) {
        std::size_t n = co_await async_read_some(source, buffer, use_awaitable);
        if (n == 0) break; // EOF

        // 处理数据
        process(buffer.data(), n);

        // 写入
        co_await async_write(destination, buffer, use_awaitable);
    }
}

6. 常见陷阱与调试技巧

问题 解决方案
协程被错误销毁 确保 coroutine_handle 只在必要时 destroy();避免悬空引用
内存泄漏 promise_typefinal_suspend() 中释放资源
调试困难 使用 -g 编译并借助 GDB 的 bt 查看协程调用栈;或使用 asio::debug 输出日志
性能瓶颈 对比 std::suspend_always vs std::suspend_never,尽量减少不必要的挂起点

7. 未来展望

  • 标准库扩展:C++23 正在讨论更完善的协程工具,例如 std::generatorstd::async 的协程化。
  • 跨语言互操作:协程在 Rust、Python、JavaScript 等语言中已经成熟,C++ 通过 cppcoroasio::awaitable 等库与其互操作。
  • 硬件加速:未来可能出现针对协程的专用指令集或调度器,以进一步提升性能。

结语

C++20 协程为处理异步 I/O、生成器以及状态机提供了更加简洁、可维护的语法。虽然学习曲线略陡峭,但掌握后能显著提升代码可读性和运行效率。希望本文能帮助你快速上手协程,在实际项目中发挥其优势。祝编码愉快!

发表评论