C++20协程:从基础到实战的异步编程指南

在C++20标准中,协程(coroutine)被正式引入,成为一种轻量级的、基于协作式多任务的异步编程工具。它们让代码更直观地描述异步流程,避免回调地狱和状态机的繁琐。本文将从协程的基础概念、关键语法、实现机制,到实战应用逐步展开,帮助你快速上手并将其融入日常项目。

1. 协程的基本概念

协程是一种可暂停和恢复的函数,它在运行时可以在任意点挂起(yield)并保存自己的执行状态,随后在需要时继续执行。与传统线程不同,协程不需要操作系统调度,所有挂起和恢复都在单线程内完成,因而开销更小。

1.1 协程与异步I/O的关系

协程可以与异步I/O配合使用,利用事件循环(Event Loop)或操作系统的I/O复用机制(如epoll、IOCP)实现非阻塞I/O。协程的挂起点会在I/O完成前暂停,避免线程阻塞,提升系统并发性能。

1.2 关键特性

  • 协作式多任务:协程由程序显式挂起/恢复,完全由代码控制。
  • 状态保存:协程在挂起时自动保存局部变量状态。
  • 可组合性:协程可以互相调用,形成链式调用或并行执行。
  • 类型安全:协程返回类型是std::coroutine_handle或自定义返回类型。

2. C++20协程的核心语法

2.1 co_awaitco_yieldco_return

  • co_await:等待一个 awaitable 对象完成,并在完成后返回其结果。若 awaitable 为空,协程立即继续。
  • co_yield:返回一个值给调用者,同时挂起协程。适用于生成器(generator)模式。
  • co_return:终止协程并返回值,若无返回值,则使用 void

2.2 std::suspend_alwaysstd::suspend_never

这两个辅助结构决定协程在启动和结束时是否立即挂起。常用于自定义协程框架。

2.3 示例:简单的异步计时器

#include <chrono>
#include <coroutine>
#include <iostream>

struct timer {
    struct promise_type {
        timer get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

timer sleep_for(std::chrono::milliseconds ms) {
    struct sleep_awaiter {
        std::chrono::milliseconds dur;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h) {
            std::thread([h, dur=dur]{
                std::this_thread::sleep_for(dur);
                h.resume();
            }).detach();
        }
        void await_resume() {}
    };
    co_await sleep_awaiter{ms};
}

此代码演示了如何创建一个异步计时器。await_suspend 在新线程中完成 sleep,随后通过协程句柄恢复。

3. 生成器(Generator)的实现

生成器是一种特殊的协程,用于按需产生一系列值。C++20 并未直接提供标准库实现,但可以利用 co_yield 自行实现。

3.1 生成器的返回类型

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

    std::coroutine_handle <promise_type> handle;

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

    T next() {
        handle.resume();
        return handle.promise().current_value;
    }

    bool has_next() const { return !handle.done(); }
};

3.2 使用示例

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

int main() {
    for (auto g = count_to(5); g.has_next(); ) {
        std::cout << g.next() << ' ';
    }
    // 输出: 0 1 2 3 4
}

4. 实战:协程与网络I/O

下面给出一个使用协程的异步 TCP 服务器示例,基于 Boost.Asio(支持协程接口)。

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

using boost::asio::ip::tcp;
using boost::asio::experimental::awaitable;
using namespace std::chrono_literals;

awaitable <void> handle_session(tcp::socket socket) {
    char data[1024];
    std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable);
    co_await boost::asio::async_write(socket, boost::asio::buffer(data, n), boost::asio::use_awaitable);
}

awaitable <void> server(unsigned short port) {
    boost::asio::io_context io_context;
    tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
    for (;;) {
        tcp::socket socket = co_await acceptor.async_accept(boost::asio::use_awaitable);
        std::jthread t([socket = std::move(socket)]() mutable {
            co_spawn(io_context, handle_session(std::move(socket)), boost::asio::detached);
        });
    }
}

int main() {
    boost::asio::co_spawn(
        boost::asio::io_context{},
        server(12345),
        boost::asio::detached);
    return 0;
}

此示例展示了如何利用 Boost.Asio 的 async_*awaitable 结合,使异步网络代码更直观。

5. 性能与注意事项

方面 说明
堆栈占用 协程不需要完整堆栈,状态由编译器生成;但递归协程会产生更大状态体。
异常安全 需要在 promise_type 中实现 unhandled_exception;协程挂起点附近的异常会传递给外层。
调试 调试器对协程的断点支持逐步改进,但在复杂链式协程中仍可能出现跳转。
兼容性 需要编译器支持 C++20 协程(GCC ≥10、Clang ≥11、MSVC 19.26)。

6. 未来展望

  • 协程池:类似线程池的协程池可进一步降低协程切换成本。
  • 多阶段协程:通过自定义 awaitable 实现分阶段任务,例如预处理、IO、后处理。
  • 协程与GCD:在iOS/macOS中,协程可以与 Grand Central Dispatch 整合,提升任务并发控制。

结语
C++20 的协程为我们提供了一种更自然、更高效的异步编程方式。只需少量语法改动即可显著提升代码可读性和性能。希望通过本文,你能快速掌握协程的核心概念与实践技巧,并在实际项目中发挥出它的巨大价值。祝编码愉快!

发表评论