C++20协程的应用与实践

C++20协程(co-routine)是对异步编程模式的一次重要升级,使得异步代码能够像同步代码一样直观、可读。本文将从协程的基本概念、实现原理到实际应用四个方面,带你系统了解如何在项目中使用C++协程。

1. 协程基础

1.1 什么是协程

协程是一种可挂起的函数,其执行可以在多次调用间切换状态。与线程不同,协程是轻量级的,并且只在单线程中切换,避免了线程切换的上下文成本。C++20 在标准库中新增了 std::coroutine_handlestd::suspend_alwaysstd::suspend_never 等工具,提供了对协程生命周期的完整控制。

1.2 语法演示

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

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

Task example() {
    std::cout << "开始\n";
    co_await std::suspend_always{};
    std::cout << "继续\n";
}

int main() {
    example();
}

这段代码展示了一个最简协程,使用 co_await 暂停执行。

2. 协程实现原理

协程的编译过程可以类比为状态机。编译器会把协程拆分成若干段,生成对应的状态机表。promise_type 保存协程的局部状态、异常信息等。协程在挂起时,调用 co_await 的 awaitable 对象会返回一个 await_suspend,该函数决定是否真正挂起。

2.1 promise_type 的角色

  • get_return_object:返回协程句柄(或包装对象)。
  • initial_suspend / final_suspend:决定协程开始时和结束时是否挂起。
  • return_void / return_value:处理返回值。
  • unhandled_exception:处理异常。

2.2 awaitable 与 awaiter

协程中 co_await 的对象必须实现 await_readyawait_suspendawait_resume 三个成员函数。

  • await_ready:立即完成返回 true 或继续等待返回 false
  • await_suspend:传入 coroutine_handle,可以决定挂起行为。
  • await_resume:等待结束后返回值。

3. 实际应用案例

3.1 异步网络请求

利用协程与 asiolibuv 结合,可实现简洁的异步 I/O。示例:

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

using namespace boost::asio;
using namespace boost::asio::experimental::awaitable_ns;

awaitable <void> tcp_client(const std::string& host, unsigned short port) {
    io_context& ioc = co_await this_coro::executor;
    ip::tcp::resolver resolver(ioc);
    auto endpoints = co_await resolver.async_resolve(host, std::to_string(port), use_awaitable);
    ip::tcp::socket socket(ioc);
    co_await async_connect(socket, endpoints, use_awaitable);
    std::string msg = "Hello, server!";
    co_await async_write(socket, buffer(msg), use_awaitable);
    co_return;
}

此代码几乎与同步写法相同,却是非阻塞的。

3.2 延时任务

协程天然支持 co_await std::chrono::steady_clock::now() + std::chrono::seconds(2) 的方式实现延时。

awaitable <void> delay_task() {
    auto start = std::chrono::steady_clock::now();
    co_await std::chrono::steady_clock::now() + std::chrono::seconds(2);
    std::cout << "延时2秒\n";
}

3.3 迭代器协程(Generator)

协程可以轻松实现惰性序列。

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> coro;
    explicit generator(std::coroutine_handle <promise_type> h) : coro(h) {}
    ~generator() { if (coro) coro.destroy(); }

    struct iterator {
        std::coroutine_handle <promise_type> coro;
        iterator(std::coroutine_handle <promise_type> h) : coro(h) {}
        iterator& operator++() {
            coro.resume();
            if (coro.done()) coro = nullptr;
            return *this;
        }
        bool operator==(std::default_sentinel_t) const { return !coro; }
        const T& operator*() const { return coro.promise().current_value; }
    };

    iterator begin() {
        coro.resume();
        if (coro.done()) return iterator{nullptr};
        return iterator{coro};
    }
    std::default_sentinel_t end() { return {}; }
};

generator <int> fib(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int tmp = a + b;
        a = b;
        b = tmp;
    }
}

使用 for (int x : fib(10)) std::cout << x << ' '; 即可输出斐波那契数列。

4. 协程使用的注意事项

  1. 异常安全promise_type::unhandled_exception 需要正确处理。
  2. 生命周期:协程句柄必须在其所持有的对象生命周期内有效。
  3. 性能:协程本身无上下文切换,但过度使用可能导致堆栈开销。
  4. 与多线程:协程本身是单线程的,若需要并发,请结合线程池或 async dispatch。

5. 结语

C++20 协程为异步编程带来了极大的便利,使得“异步”代码写法与同步代码保持一致的风格,极大提升了可读性与可维护性。通过正确理解协程的实现机制、熟悉 promise/promise_type 的使用,并结合现代异步 I/O 库,你可以在项目中快速构建高性能、低延迟的网络服务或其他异步功能。随着标准库和第三方生态的完善,协程将在 C++ 开发者的日常工作中发挥越来越重要的作用。

发表评论