**C++20 中的 coroutines:让异步编程更简洁**

C++20 引入了 coroutine(协程)这一强大的语法特性,彻底改变了我们对异步编程的思考方式。传统的异步实现往往依赖回调、事件循环或线程池,代码可读性差、错误率高,而协程通过让函数“挂起”和“恢复”,让异步流程像同步流程一样直观。本文将从概念入手,逐步拆解协程的实现细节,并给出实战示例,帮助读者快速掌握并在项目中落地。


1. 协程基础概念

  • 挂起点(Suspend Point):在协程中,co_awaitco_yieldco_return 等关键字会产生挂起点,函数在此处暂停执行。
  • 协程句柄(Coroutine Handle):每个协程都有一个句柄,用来管理其生命周期、恢复执行以及访问结果。句柄类型通常为 std::coroutine_handle<>
  • 悬挂对象(Suspension Object)co_await 后面跟随的对象负责决定协程是否挂起以及挂起时的行为。常见的悬挂对象有 std::suspend_alwaysstd::suspend_never 等。

2. 协程的基本语法

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

Task foo() {
    std::cout << "开始\n";
    co_await std::suspend_always{};   // 挂起点
    std::cout << "恢复\n";
}

int main() {
    foo();  // 仅创建协程,实际未执行
    return 0;
}
  • promise_type:每个协程必须实现一个 promise_type,它定义了协程生命周期中的行为(挂起、返回、异常处理等)。
  • initial_suspendfinal_suspend:分别控制协程开始前和结束后的挂起行为。

3. 典型应用场景

3.1 异步 I/O

使用 co_await 等待底层 I/O 完成,避免回调地狱。例如结合 Boost.Asio 的 awaitable

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

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

3.2 并行流水线

将多个协程串联起来,形成数据处理流水线,天然支持异步等待与并行执行。

struct Frame {
    int id;
    // 其它数据...
};

Frame decode(const std::vector <char>& raw) { /* ... */ }

boost::asio::awaitable <Frame> process_frame(const std::vector<char>& raw) {
    Frame f = co_await boost::asio::async_invoke(decode, raw, boost::asio::use_awaitable);
    // 进一步处理...
    co_return f;
}

4. 协程与线程的区别

维度 线程 协程
开销 高(上下文切换、堆栈管理) 低(协程上下文仅为少量寄存器与状态)
可读性 难以直观展示异步流程 如同步流程,易维护
并发模型 OS 调度 由程序员手动调度或库实现
互斥 需要锁 可通过 std::atomicawaitable 处理

5. 实战:基于协程的简易 HTTP 服务器

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

namespace asio = boost::asio;
using tcp = asio::ip::tcp;
using asio::awaitable;

awaitable <void> handle_session(tcp::socket socket) {
    char data[4096];
    std::size_t n = co_await socket.async_read_some(asio::buffer(data), asio::use_awaitable);
    std::string request(data, n);
    std::cout << "请求: " << request << "\n";

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

awaitable <void> server(asio::io_context& ctx, unsigned short port) {
    tcp::acceptor acceptor(ctx, tcp::endpoint(tcp::v4(), port));
    while (true) {
        tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
        asio::co_spawn(ctx, handle_session(std::move(socket)), asio::detached);
    }
}

int main() {
    asio::io_context ctx;
    asio::co_spawn(ctx, server(ctx, 8080), asio::detached);
    ctx.run();
}
  • co_spawn:把协程放入事件循环中执行。
  • asio::use_awaitable:让 async_* 函数返回 awaitable,可直接 co_await

6. 常见坑与调试技巧

  1. 协程句柄泄露
    co_await 后返回的句柄若未显式销毁,可能导致资源泄露。常用的做法是使用 std::unique_ptrstd::coroutine_handle<>::destroy()

  2. 异常传播
    协程内部异常需在 promise_type::unhandled_exception() 处理,否则会终止程序。可使用 std::exception_ptr 保存异常并在外层 co_await 时重新抛出。

  3. 调试

    • 通过 -g 编译,使用 GDB info coroutine 查看协程状态。
    • 结合 asio::debug 打印事件循环日志。

7. 结语

C++20 协程的出现,是语言演进中的一次重大跃迁。它将异步编程的“隐式”变为“显式”,使代码可读性大幅提升,错误率显著下降。掌握协程的核心概念与实践技巧后,你将能更轻松地实现高性能、低延迟的网络服务、并发计算以及复杂事件驱动系统。希望本文能为你打开协程世界的大门,开启更高效、更优雅的编程旅程。

发表评论