C++20 协程(Coroutines)在异步编程中的应用

在 C++20 之前,C++ 的异步编程往往依赖于回调、事件循环或第三方库(如 Boost.Asio)。这些方式虽然功能强大,却常常导致代码结构复杂、错误难以追踪。C++20 引入了 协程(Coroutines),为异步编程提供了更直观、更接近同步语义的写法。本文将从协程的基本概念入手,展示如何利用协程简化异步任务,进而在实际项目中实现高性能、可维护的异步系统。


1. 协程基础

1.1 什么是协程?

协程是一种轻量级的、可挂起的函数,它能够在执行过程中暂停(co_awaitco_yieldco_return)并在之后恢复执行。与传统线程相比,协程的上下文切换成本极低,能够在单线程或多线程环境中实现高效的并发。

1.2 C++20 协程的关键字

  • co_await:等待一个可等待对象的完成。
  • co_yield:生成一个值,并挂起协程。
  • co_return:返回协程的最终结果。

1.3 协程的返回类型

C++20 的协程返回类型不是普通的 T,而是一个 promise type(承诺类型)与 awaiter 之间的桥梁。常见的协程返回类型包括:

  • `std::future `
  • `std::generator `(从 C++23 开始)
  • 自定义类型(如 `Task `)

2. 一个简单的异步 IO 示例

假设我们使用标准库中的 std::filesystem 进行文件读取,并希望异步读取文件内容。下面给出一个基于 std::future 的协程实现。

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

namespace fs = std::filesystem;

// 简单的异步读取文件内容
std::future<std::string> async_read_file(const fs::path& path)
{
    // 内部协程返回 std::future<std::string>
    co_return []() -> std::string {
        std::ifstream ifs(path);
        if (!ifs) return "读取失败";

        std::string content((std::istreambuf_iterator <char>(ifs)),
                             std::istreambuf_iterator <char>());
        return content;
    }();
}

在上面代码中,async_read_file 使用 lambda 封装同步读取逻辑,然后将其包装为 std::future。调用者可以使用 future.get() 获得结果,或者使用 co_await 进一步串联协程。


3. 通过 co_await 组合多个异步任务

C++20 协程的强大之处在于它能让异步任务像同步代码那样串联。下面演示如何使用 co_await 并行等待两个网络请求。

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

std::future <int> async_fetch_data(int id)
{
    co_return []() -> int {
        std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
        return id * 10;
    }();
}

std::future <int> aggregate_data()
{
    int a = co_await async_fetch_data(1);
    int b = co_await async_fetch_data(2);
    co_return a + b;
}

int main()
{
    auto fut = aggregate_data();
    std::cout << "聚合结果: " << fut.get() << std::endl;
}

上述代码中,aggregate_data 协程依次 co_await 两个异步请求,并在两者完成后返回结果。若想并行执行,可以改为:

std::future <int> aggregate_data_parallel()
{
    auto fut1 = async_fetch_data(1);
    auto fut2 = async_fetch_data(2);
    int a = co_await fut1;
    int b = co_await fut2;
    co_return a + b;
}

这样就可以实现真正的并行等待,提升效率。


4. 与线程池结合

在高并发服务端中,协程与线程池配合可以极大提升资源利用率。下面给出一个简易的线程池与协程协作的例子。

#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>

class ThreadPool
{
public:
    ThreadPool(size_t n);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        -> std::future<typename std::invoke_result_t<F, Args...>>;

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop = false;
};

ThreadPool::ThreadPool(size_t n)
{
    for(size_t i = 0; i < n; ++i)
        workers.emplace_back([this]{
            for(;;){
                std::function<void()> task;
                {
                    std::unique_lock<std::mutex> lock(this->queue_mutex);
                    this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); });
                    if(this->stop && this->tasks.empty())
                        return;
                    task = std::move(this->tasks.front());
                    this->tasks.pop();
                }
                task();
            }
        });
}

ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
    -> std::future<typename std::invoke_result_t<F, Args...>>
{
    using return_type = typename std::invoke_result_t<F, Args...>;
    auto task = std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward <F>(f), std::forward<Args>(args)...));
    std::future <return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");
        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

使用方式:

ThreadPool pool(4);

std::future <int> fut = pool.enqueue([]{ return 42; });
int result = fut.get();

在协程中,我们可以将线程池的 enqueue 返回值 std::futureco_await 配合使用,进一步简化异步流程。


5. 实战案例:异步 HTTP 客户端

cpp-httplib 为例,结合 C++20 协程实现一个异步 HTTP 客户端。下面展示核心思路(省略错误处理与完整实现):

#include <httplib.h>
#include <future>
#include <string>
#include <iostream>

std::future<std::string> http_get_async(const std::string& url)
{
    co_return []() -> std::string {
        httplib::Client cli("http://example.com");
        auto res = cli.Get("/");
        if (res && res->status == 200)
            return res->body;
        return "请求失败";
    }();
}

int main()
{
    auto fut = http_get_async("http://example.com");
    std::cout << "响应: " << fut.get() << std::endl;
}

通过 co_await,我们可以把多个 HTTP 请求串联起来:

std::future <void> fetch_multiple()
{
    std::string body1 = co_await http_get_async("http://example.com/a");
    std::string body2 = co_await http_get_async("http://example.com/b");
    // 处理结果
}

6. 性能与可维护性

  • 性能:协程的上下文切换成本极低(大约为函数调用的 1/10),相比线程切换(数十微秒)能够显著提升并发吞吐量。
  • 可维护性:使用 co_await 的代码结构与同步代码极为相似,阅读和调试更直观。
  • 错误处理:协程与异常协作良好,可以直接使用 try/catch 捕获异步错误。

7. 小结

C++20 协程为 C++ 提供了一套完整、原生的异步编程模型。通过 co_awaitco_yieldco_return,我们可以在保持同步语义的前提下,构建高效、可组合的异步任务。结合线程池、异步 IO 或第三方网络库,协程能够帮助我们写出既简洁又高性能的异步代码。未来,随着 C++23、C++26 的标准化,协程生态将更加完善,值得开发者积极探索。

发表评论