C++20中的协程:实现异步任务的全新方式

在现代C++中,协程(coroutine)为我们提供了一种简洁而强大的方式来编写异步、事件驱动或并发代码。自C++20起,协程被正式纳入标准库,配合std::generatorstd::taskstd::suspend_always等辅助类,让异步编程更像同步代码。本文将深入剖析协程的工作原理、关键概念以及实际使用技巧,并通过示例代码展示如何在C++20项目中实现异步任务。

1. 协程的基本概念

  • 协程函数:一种特殊的函数,它可以在执行过程中被挂起和恢复。协程函数的声明需要返回类型为std::experimental::coroutine_handle相关的类型,例如std::future, std::generator等。
  • 挂起点:协程内部使用co_await, co_yieldco_return语句触发挂起。挂起时,协程的局部状态会被保存,直到下次恢复。
  • 协程句柄coroutine_handle是对协程实例的可操作句柄,负责调度协程的执行和销毁。

2. 关键实现细节

2.1 协程的状态机

编译器会把协程函数编译成一个状态机。每个挂起点对应一个状态值,协程在恢复时根据当前状态决定执行哪一段代码。

2.2 协程框架中的promise_type

每个协程都有一个与之关联的promise_type。它是协程的“宿主”,负责提供协程的返回值、异常处理以及挂起点的行为。promise_type需要实现以下成员:

  • get_return_object()
  • initial_suspend()
  • final_suspend()
  • return_value(T)
  • unhandled_exception()

2.3 co_awaitco_yieldco_return

  • co_await expr:等待expr的完成。expr必须返回一个可 awaitable 的对象。
  • co_yield expr:生成一个值并挂起协程,等待下一个 co_yield 或外部调用。
  • co_return expr:结束协程,返回最终值。

3. 常用协程包装

3.1 std::generator

std::generator <int> count_up_to(int n) {
    for (int i = 1; i <= n; ++i)
        co_yield i;
}

3.2 std::task(自定义)

标准库中并未直接提供task,但可以自定义一个类似于std::future的协程返回类型。

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::rethrow_exception(std::current_exception()); }
    };
};

4. 示例:异步下载文件

下面演示如何使用协程实现一个简单的异步下载器。我们将使用cppcoro::async_http_client(第三方库)来演示协程与网络 I/O 的结合。

#include <cppcoro/async_task.hpp>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/async_pipe.hpp>
#include <cppcoro/http_client.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>

namespace fs = std::filesystem;

cppcoro::async_task <void> download_file(const std::string& url, const fs::path& out_path) {
    cppcoro::http_client client(url);
    co_await client.send_request(cppcoro::http::verb::get);

    std::ofstream ofs(out_path, std::ios::binary);
    if (!ofs.is_open()) {
        std::cerr << "Failed to open output file.\n";
        co_return;
    }

    auto stream = client.body_stream();
    while (auto chunk = co_await stream.next()) {
        ofs.write(chunk.data(), chunk.size());
    }

    std::cout << "Download completed: " << out_path << "\n";
}

int main() {
    cppcoro::sync_wait(download_file("https://example.com/file.bin", "file.bin"));
}

说明

  • `cppcoro::async_task ` 是一个协程返回类型,类似于 `std::future`。
  • co_await client.send_request(...) 让协程挂起,等待 HTTP 请求完成。
  • stream.next() 挂起直到下一个数据块可用,实现流式读取。

5. 常见陷阱与调试技巧

  1. 忘记 initial_suspendfinal_suspend:如果返回 std::suspend_always,协程会在创建时立即挂起,需要手动调用 handle.resume()
  2. 异常泄露promise_type::unhandled_exception() 必须处理异常,否则会导致未定义行为。
  3. 资源泄漏:协程结束前请确保所有资源(如文件句柄、网络连接)已释放。
  4. 调试工具:IDE 里可以通过设置断点在 co_await 行查看挂起点;或者使用 std::experimental::coroutine_traits 打印状态机信息。

6. 性能考量

  • 协程的开销:与传统回调相比,协程的上下文切换更轻量,且避免了大量堆分配。
  • 内存占用:协程的局部变量会在协程框架中存储;如果变量很大,考虑使用 std::shared_ptrstd::unique_ptr
  • 与线程池结合:可以将协程与线程池(如 cppcoro::io_context)结合,让 I/O 任务在后台线程上运行。

7. 结语

C++20 的协程为我们提供了一种与同步代码风格相近、易于维护的异步编程方式。通过理解其背后的状态机、promise_type 以及挂起点,开发者可以在不牺牲性能的前提下实现复杂的异步逻辑。未来的标准库(C++23 之后)将进一步完善协程相关工具,如 std::generatorstd::task 等,让协程生态更加完整。掌握这些技术,您将能在网络编程、游戏开发、金融交易等领域写出更高效、可读性更好的代码。

发表评论