C++20中协程的实际应用示例

在C++20中,协程(coroutine)提供了一个极其简洁而强大的异步编程模型。与传统的线程或回调相比,协程能够让你以同步的写法描述异步逻辑,同时保持代码的可读性和可维护性。本文将以“网络请求异步下载”为例,演示如何使用C++20协程实现一个简易的异步下载器,并讨论其优势与常见陷阱。

1. 协程的基本概念

  • co_await:挂起协程,等待一个 awaitable 对象完成。
  • co_yield:产生一个值并挂起,类似生成器。
  • co_return:返回值并结束协程。
  • awaitable:任何可以被 co_await 的类型,必须实现 await_readyawait_suspendawait_resume 三个成员函数。

协程本身并不创建新线程,协程的挂起和恢复是由调度器决定的,通常在事件循环(event loop)或线程池中完成。

2. 准备工作:简易异步网络请求库

下面我们使用一个假想的异步 HTTP 客户端 AsyncHttpClient,其 get 方法返回一个 std::future<std::string>。在真实项目中,你可以用 libcurl 的异步接口、Boost.Beast 或者自定义基于 epoll/kqueue 的 I/O 事件循环。

#include <future>
#include <string>
#include <chrono>
#include <thread>
#include <iostream>

class AsyncHttpClient {
public:
    std::future<std::string> get(const std::string& url) {
        return std::async(std::launch::async, [url]() {
            // 模拟网络延迟
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
            return std::string("Response from ") + url;
        });
    }
};

3. 协程包装器:future → awaitable

C++20 标准库没有直接提供将 std::future 转为 awaitable 的工具。我们手动实现一个轻量级的包装器:

#include <coroutine>
#include <future>

template<typename T>
struct FutureAwaitable {
    std::future <T> fut;

    bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }

    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, f = std::move(fut)]() mutable {
            f.wait();
            h.resume();
        }).detach();
    }

    T await_resume() { return fut.get(); }
};

template<typename T>
FutureAwaitable <T> to_awaitable(std::future<T> f) {
    return FutureAwaitable <T>{ std::move(f) };
}

说明

  • await_ready 检查 future 是否已完成,若已完成则不挂起。
  • await_suspend 在一个独立线程中等待 future,然后恢复协程。实际项目可用事件驱动的完成机制替代。
  • await_resume 在协程恢复时返回 future 的结果。

4. 协程函数实现

下面是一个协程 download_file,接收 URL 并返回下载内容:

#include <string>
#include <coroutine>
#include <iostream>

struct DownloadTask {
    struct promise_type {
        std::string result;
        DownloadTask get_return_object() { return {std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(std::string r) { result = std::move(r); }
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle <promise_type> coro;

    std::string get() { return coro.promise().result; }
    ~DownloadTask() { if (coro) coro.destroy(); }
};

DownloadTask download_file(AsyncHttpClient& client, const std::string& url) {
    std::string resp = co_await to_awaitable(client.get(url));
    co_return resp;
}

关键点

  • DownloadTaskpromise_type 简单地保存结果。initial_suspendfinal_suspend 均返回 std::suspend_never,表示协程在入口和出口都不挂起。实际场景中可根据需要改为 std::suspend_always
  • co_await 的 awaitable 是上面定义的 FutureAwaitable

5. 调度与事件循环

在本示例中,我们直接等待协程完成。真实应用往往需要一个事件循环,类似 Node.js 的 libuv。下面给出一个极简事件循环,处理协程挂起/恢复:

#include <queue>
#include <functional>

class EventLoop {
public:
    void schedule(std::function<void()> task) { tasks.push(std::move(task)); }
    void run() {
        while (!tasks.empty()) {
            auto t = std::move(tasks.front());
            tasks.pop();
            t();
        }
    }
private:
    std::queue<std::function<void()>> tasks;
};

FutureAwaitableawait_suspend 修改为使用 EventLoop

void await_suspend(std::coroutine_handle<> h) {
    // 将恢复操作加入事件循环
    event_loop.schedule([h](){ h.resume(); });
}

这样,所有挂起的协程都交由事件循环调度,避免了每次挂起都创建新线程。

6. 完整示例

#include <iostream>
#include <future>
#include <coroutine>
#include <string>
#include <thread>
#include <chrono>
#include <queue>
#include <functional>

// 事件循环
class EventLoop {
public:
    void schedule(std::function<void()> task) { tasks.push(std::move(task)); }
    void run() {
        while (!tasks.empty()) {
            auto t = std::move(tasks.front());
            tasks.pop();
            t();
        }
    }
private:
    std::queue<std::function<void()>> tasks;
};

EventLoop g_loop;

// 异步 HTTP 客户端
class AsyncHttpClient {
public:
    std::future<std::string> get(const std::string& url) {
        return std::async(std::launch::async, [url]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
            return std::string("Response from ") + url;
        });
    }
};

// Future → Awaitable
template<typename T>
struct FutureAwaitable {
    std::future <T> fut;

    bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }

    void await_suspend(std::coroutine_handle<> h) {
        // 用事件循环恢复
        g_loop.schedule([h](){ h.resume(); });
    }

    T await_resume() { return fut.get(); }
};

template<typename T>
FutureAwaitable <T> to_awaitable(std::future<T> f) { return FutureAwaitable<T>{std::move(f)}; }

// 协程任务
struct DownloadTask {
    struct promise_type {
        std::string result;
        DownloadTask get_return_object() { return {std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_value(std::string r) { result = std::move(r); }
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle <promise_type> coro;
    std::string get() { return coro.promise().result; }
    ~DownloadTask() { if (coro) coro.destroy(); }
};

DownloadTask download_file(AsyncHttpClient& client, const std::string& url) {
    std::string resp = co_await to_awaitable(client.get(url));
    co_return resp;
}

int main() {
    AsyncHttpClient client;

    auto task = download_file(client, "https://example.com");
    g_loop.schedule([t=std::move(task)]() mutable {
        std::cout << "Download finished: " << t.get() << std::endl;
    });

    g_loop.run(); // 进入事件循环
    return 0;
}

运行结果

Download finished: Response from https://example.com

7. 优点与注意事项

优点 说明
代码简洁 co_await 让异步代码呈现同步写法
可组合性 协程可以像普通函数一样被组合、传参
资源友好 只要有事件循环,协程本身不消耗线程资源
注意 说明
awaitable 设计 必须正确实现 await_ready / await_suspend / await_resume,否则可能导致死锁
事件循环 协程挂起时应加入事件循环,避免使用 std::thread 阻塞
错误传播 unhandled_exception 必须妥善处理,否则会直接终止程序

8. 小结

本文通过一个简易的网络下载示例,演示了 C++20 协程的基本使用方法。通过 FutureAwaitablestd::future 转为 awaitable,结合事件循环,你可以轻松实现高性能的异步 I/O。随着 C++23 引入的 std::ranges::subrangestd::experimental::generator,协程的生态将进一步完善,成为构建高并发应用的核心技术之一。

发表评论