一、前言

网络编程的程序并发处理能力、系统资源的利用率非常重要。不管是构建高性能的网络服务器、响应迅速的桌面应用,还是处理海量数据的批处理系统,都要异步编程。异步是指在等待耗时操作完成的同时,继续执行其他任务,避免界面卡顿、提高吞吐量,充分使用多核处理器资源。

C++ 作为系统级编程语言,一生都在追求性能和控制力,异步编程也在不断演进。从最开始的基于裸线程和回调函数的手动管理,到 C++11 引入的 std::threadstd::async 以及 std::future/std::promise 机制,极大简化了并发任务的创建和结果获取。但是,异步逻辑日益复杂的今天,std::future/std::promise 在组合性和链式调用方面的局限性逐渐显现,有时甚至会导致难以维护的“回调地狱”问题。为了解决这些问题,C++20 标准引入了革命性的协程(Coroutines)语言特性,可以更直观、更高效的编写异步代码。

本文对比 C++11 以来广泛使用的 std::future/std::promise 机制和 C++20 新增的协程特性,阐明为何协程被认为是现代 C++ 异步编程的未来方向,并针对 C++20 标准库在协程方面提供的基础性支持,推荐一些优秀的第三方协程库。

二、C++11 的 future 和 promise

C++11 标准引入的 std::futurestd::promise 非常强大,简化并发编程中异步任务的结果传递和同步问题。提供比裸线程和条件变量更高级、更易用的抽象。

相关概念:

  1. std::promise:异步操作的“写入端”。 std::promise 对象存储一个值或一个异常,在某个时刻被异步的提供。提供有set_value() 方法可以设置结果值,以及 set_exception() 方法设置一个异常。只要设置了值或异常,std::promise 就变成“已完成”。每个 std::promise 对象都关联一个 std::future 对象,通过 get_future() 方法可以获取到这个关联的 std::future
  2. std::future:异步操作的“读取端”。 std::future 对象是 std::promise 对象的“读取端”。代表一个可能在未来某个时间点可用的结果。通过 std::futureget() 方法,可以阻塞当前线程直到关联的 std::promise 设置了结果或异常,然后获取到该结果或重新抛出异常。std::future 还提供了 wait()wait_for() 等非阻塞等待方法,检查结果是否可用。
  3. std::async:封装了线程创建、任务执行以及 std::promisestd::future 的管理。可以以函数调用的方式启动一个异步任务,并直接返回一个 std::future 对象,不需要手动创建 std::promisestd::threadstd::async 默认会根据系统负载选择是立即执行任务(std::launch::deferred)还是在新线程中异步执行(std::launch::async)。

优点:

  1. 对于简单的异步任务,std::futurestd::promise 是一种清晰且易于理解的模式:一个生产者(promise)提供结果,一个消费者(future)获取结果。结合 std::async,非常简洁。
  2. std::futurestd::promise 不需要任何第三方库即可使用。
  3. 适合简单的、独立的异步任务。 任务是独立的,结果只需要获取一次,且后续没有复杂的依赖或链式操作时,std::future/std::promise 就非常的合适。
任务启动器
消费者
生产者
设置结果 (set_value/exception)
获取结果 (get)
get_future()
返回 std::future
std::async
std::future
std::promise
共享状态 Shared State

缺点:

  1. 不能清晰的表达复杂的异步流程(无 .then() 链式调用),这是 std::future/std::promise 最主要的局限。没有提供类似 JavaScript Promise 或其他异步框架中的 .then() 方法,无法直接把多个异步操作串联起来形成一个链式调用。如果一个异步任务的结果是另一个异步任务的输入,或者要等待多个异步任务都完成后才能继续,就不得不引入额外的同步机制(std::packaged_taskstd::shared_future,或者手动管理线程和条件变量),这样一来代码就变得复杂了。
  2. std::future::get() 方法是阻塞的。调用 get() 时结果还没有准备好,当前线程就被暂停执行,直到结果可用。这在同时处理多个异步操作或保持 UI 响应性的场景下是不能接受的。虽然有 wait()wait_for(),但要手动轮询或超时处理。
  3. std::futureget() 方法只能被调用一次。一旦调用,std::future 对象就变为“无效”的,不能再次获取结果。虽然 std::shared_future 可以解决多次获取结果的问题,但还是不能解决组合性差和阻塞性问题。
  4. 为了避免 get() 的阻塞,会去用回调函数。在没有语言层面支持的情况下,手动管理回调会导致层层嵌套的“回调地狱”。或者,为了实现复杂的依赖关系,不得不编写大量的互斥量、条件变量等同步原语,增加代码的复杂性。

适用场景:

  1. 启动一个独立的、无需复杂后续操作的后台任务。
  2. 作为协程内部或更高级抽象的底层构建块。

三、C++20 协程 (Coroutines)

C++20 引入的协程(Coroutines)是语言层面的一次重大革新,为异步编程带来革命性的改变。协程是编写非阻塞、并发代码的新范式,让异步逻辑的表达更加直观、可读,能解决异步编程常见的“回调地狱”和复杂同步机制的问题。

协程是一种用户态的轻量级并发执行单元。跟线程不同,协程的调度完全由程序员控制,而不是由操作系统内核控制。一个协程可以在执行过程中暂停(suspend),控制权交还给调用者或调度器,然后在某个时刻从暂停点恢复(resume)执行。这种“暂停-恢复”的能力是协程的核心特征。协程是“栈无关”的,没有自己的独立栈空间,而是用调用者的栈空间,这可以大大降低内存开销和上下文切换的成本。

用户态
操作系统/内核
调度
1.调用/启动协程
2.执行代码
3.遇到暂停点 (co_await\co_yield)
4.控制权交还
5.程序员/调度器控制
6.执行代码
7.遇到暂停点
8.控制权交还
9.程序员/调度器决定
10.从暂停点继续执行
11.协程完成/返回
协程 Coroutine A
协程 Coroutine B
代码段1
暂停 Coroutine A
代码段1
暂停 Coroutine B
恢复 Coroutine A
代码段2
线程 (Thread)
主程序/调度器

C++20 引入三个新的关键字来支持协程:

  • co_await:暂停当前协程的执行,直到一个“可等待对象”完成。可等待对象完成时,协程会从暂停点恢复执行。这是协程实现非阻塞等待的关键。
  • co_yield:主要用来实现生成器。可以让协程在每次生成一个值时暂停,在下次请求值时恢复。异步编程对co_yield 的使用相对较少,更常见的是 co_await
  • co_return:从协程中返回一个值或表示协程执行结束。类似普通函数的 return 语句,但它是协程特有的返回机制。

C++20 协程的实现依赖编译器,编译器把协程函数转换为一个状态机。这个状态机包含协程的局部变量、参数以及执行的当前位置。协程被暂停时,状态会被保存;协程恢复时,状态会被加载,并从上次暂停的地方继续执行。

为了让协程能跟外部世界交互,C++20 协程机制引入了几个关键概念:

(1)Promise Type。 每个协程都必须有一个关联的 Promise Type。这个类型由协程的返回类型决定,并通过模板特化来定义。Promise Type 是协程和外部世界交互的桥梁,定义了协程的生命周期管理、异常处理以及如何获取协程的返回值。Promise Type 必须实现几个关键成员函数:

  • get_return_object(): 返回一个“协程结果对象”(std::future 的替代品),供调用者获取协程的最终结果。
  • initial_suspend(): 定义协程在首次进入时是否立即暂停。
  • final_suspend(): 定义协程在执行完毕或抛出异常后是否暂停。
  • return_value(T value) / return_void(): 处理 co_return 语句返回的值。
  • unhandled_exception(): 处理协程内部未捕获的异常。

(2)Awaitable 和 Awaiter。 co_await 表达式后面跟着的对象称为“可等待对象”(Awaitable)。一个可等待对象要提供一个“等待器”(Awaiter)对象。Awaiter 是实际执行暂停和恢复逻辑的实体,必须实现三个核心方法:

  • await_ready(): 判断是否需要暂停。如果返回 true,则不暂停,直接执行 await_resume()
  • await_suspend(std::coroutine_handle<Promise> handle): 执行暂停操作。把当前协程的句柄(handle)保存起来,方便在异步操作完成后恢复协程。
  • await_resume(): 在协程恢复后执行。获取异步操作的结果或处理异常。

(3)std::coroutine_handle,轻量级的非拥有类型,指向协程的内部状态。通过 std::coroutine_handle,可以手动恢复(resume())协程的执行,或者销毁(destroy())协程的状态。它是连接异步操作完成与协程恢复的关键。

C++20 协程机制完美地弥补了 std::future/std::promise 在组合性和非阻塞性方面的不足:

  1. co_await 可以像编写同步代码一样,串联多个异步操作。一个异步操作的结果可以直接作为下一个异步操作的输入,不再需要嵌套回调或复杂的同步原语。
  2. 协程遇到 co_await 表达式时,如果被等待的异步操作尚未完成,协程会立即暂停执行,把控制权交还给调用者或事件循环。当前线程不会被阻塞,可以继续执行其他任务。当异步操作完成后,协程会被恢复,从 co_await 点继续执行。
  3. 协程的异步代码看起来和写起来都像同步代码。这种“线性化”的编程风格消除了回调函数的层层嵌套。
  4. 协程可以在函数体的任意位置暂停和恢复,比线程更细粒度的控制能力。

协程的优势:

  1. 异步逻辑以顺序方式表达。
  2. 栈无关,上下文切换开销远小于线程,且可以创建大量的协程而不会耗尽系统资源。一个线程可以同时管理成千上万个协程。
  3. 在函数内部进行非阻塞的等待和控制流的转移。
  4. 减少操作系统层面的线程调度和上下文切换,带来更好的性能。

协程很好,但是,也不可避免的出现一些缺点:

  1. C++20 协程的底层机制比较复杂,学习起来比较困难,要一定时间去理解工作原理。
  2. C++20 标准只提供协程的底层语言原语,没有直接提供高级的异步框架。
  3. 协程的执行是非线性的,堆栈跟踪不能直观反映协程的完整调用链,调试起来会比较的复杂。
  4. 相比其他语言(C#、JavaScript、Python)成熟的异步/协程生态系统,C++ 的协程生态才刚起步。

使用协程的简单示例:

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

using namespace std;

// 通用模板
template<typename T>
struct MyTask {
    struct promise_type {
        T value_;
        std::exception_ptr exception_;
        // 存储非 void 类型的值
        std::coroutine_handle<> continuation_; // 存储恢复点

        MyTask get_return_object() { return MyTask{std::coroutine_handle<promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; } // 协程开始时暂停
        std::suspend_always final_suspend() noexcept { return {}; } // 协程结束时暂停

        void return_value(T value) { value_ = value; }
        void unhandled_exception() { exception_ = std::current_exception(); }
    };

    std::coroutine_handle<promise_type> handle_;

    MyTask(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~MyTask() { if (handle_) handle_.destroy(); } // 销毁协程状态

    T get() {
        handle_.resume();
        if (handle_.promise().exception_) {
            std::rethrow_exception(handle_.promise().exception_);
        }
        return handle_.promise().value_;
    }
    
    // 实现 co_await 支持
    bool await_ready() const { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        handle_.promise().continuation_ = h;
        handle_.resume();
    }
    T await_resume() {
        if (handle_.promise().exception_) {
            std::rethrow_exception(handle_.promise().exception_);
        }
        return handle_.promise().value_;
    }
};

// void 特化版本
template<>
struct MyTask<void> {
    struct promise_type {
        std::exception_ptr exception_;
        std::coroutine_handle<> continuation_; // 存储恢复点

        MyTask get_return_object() { return MyTask{std::coroutine_handle<promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; } // 协程开始时暂停
        std::suspend_always final_suspend() noexcept { return {}; } // 协程结束时暂停

        void return_void() {} // 处理 void 返回类型
        void unhandled_exception() { exception_ = std::current_exception(); }
    };

    std::coroutine_handle<promise_type> handle_;

    MyTask(std::coroutine_handle<promise_type> h) : handle_(h) {}
    ~MyTask() { if (handle_) handle_.destroy(); } // 销毁协程状态

    void get() {
        handle_.resume();
        if (handle_.promise().exception_) {
            std::rethrow_exception(handle_.promise().exception_);
        }
    }
    
    // 实现 co_await 支持
    bool await_ready() const { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        handle_.promise().continuation_ = h;
        handle_.resume();
    }
    void await_resume() {
        if (handle_.promise().exception_) {
            std::rethrow_exception(handle_.promise().exception_);
        }
    }
};

MyTask<std::string> async_fetch_data(const std::string& url) {
    std::cout << "正在从 " << url << " 异步获取数据..." << std::endl;

    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "数据获取完成: " << url << std::endl;
    co_return "从 " + url + " 获取到的数据";
}

MyTask<int> async_process_data(const std::string& raw_data) {
    std::cout << "正在异步处理数据: " << raw_data.substr(0, 10) << "..." << std::endl;
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    int result = static_cast<int>(raw_data.length()); // 简单处理
    std::cout << "数据处理完成,结果: " << result << std::endl;
    co_return result;
}

// 主异步逻辑
MyTask<void> main_async_flow() {
    std::cout << "主异步流开始。" << std::endl;

    std::string data = co_await async_fetch_data("Lion, King of Beasts.");
    std::cout << "在 main_async_flow 中收到数据: " << data << std::endl;

    int processed_result = co_await async_process_data(data);
    std::cout << "在 main_async_flow 中收到处理结果: " << processed_result << std::endl;

    std::cout << "主异步流结束。" << std::endl;
    co_return; // 协程结束
}

int main() {
    std::cout << "程序开始运行。" << std::endl;

    // 启动主异步流
    MyTask<void> task = main_async_flow();

    task.get();

    std::cout << "程序运行结束。" << std::endl;
    return 0;
}

四、选择大于努力

不同维度的对比:

特性 std::future / std::promise C++20 协程 (Coroutines)
可读性 较低,特别是在复杂异步流程,容易形成“回调地狱”或复杂的同步逻辑。 极高,用同步线性风格编写异步代码。
性能 关乎线程创建和销毁,以及操作系统层面的上下文切换,开销相对较大。 用户态上下文切换,开销远低于线程切换,可创建大量协程而不耗尽资源,性能潜力巨大。
组合性 差,缺乏直接的链式调用机制 。 极佳,co_await 机制可以像同步代码一样串联、并行、条件等待多个异步操作。
学习成本 较低,概念相对简单,易于上手。 较高,需要理解 promise_typeawaitableawaiter 等底层概念,以及协程的生命周期管理。
标准库支持度 完全由标准库提供,开箱即用。 只提供语言层面的底层原语,缺乏高级的协程类型、调度器和异步 I/O 适配,依赖第三方库。

虽然 std::future/std::promise 还有用武之地,但 C++20 协程已经是现代 C++ 异步编程的未来。随着标准库和第三方库对协程生态的不断完善(特别是std::execution),C++ 的异步编程必然更加强大和易用。

现在完全抛弃之前的异步代码,全面转向协程还不太现实。但可以把协程和 std::future/std::promise 混合使用,实现一个过渡和兼容。

五、C++20 协程库推荐

C++20 标准库对协程的支持是在语言层面的,只提供了语言层面的原语(co_await, co_yield, co_returnstd::coroutine_handle ),还没有提供开箱即用的高级组件。

  1. 标准库没有提供像 task<T> 这样的通用协程返回类型。要自己实现或使用库提供的 promise_type
  2. 协程要一个机制来调度执行,特别是在 co_await 暂停后恢复时。标准库还没有提供通用的执行器或调度器。
  3. 网络、文件等异步 I/O 操作是异步编程的核心,但标准库没有提供协程友好的异步 I/O 接口。
  4. 协程间的同步也要协程友好的版本,避免阻塞调度器。
  5. 虽然协程支持异常传播,但更高级的错误处理和任务取消机制还要库来提供。

所以,第三方库来填补这些空白,提供完整的协程生态系统。

当前 C++ 协程领域主流协程库:

(1)cppcoro 由 C++ 协程提案的主要作者之一 Lewis Baker 主导开发的库。提供一套设计优雅、基础且高效的协程类型和辅助工具。是协程设计的“事实标准”和学习协程底层机制的优秀参考。只包含核心的协程抽象,不绑定特定的 I/O 模型或调度器。提供 task<T>generator<T>async_mutexasync_manual_reset_event 等基础协程类型和同步原语。适合作为其他协程库或框架的基础,或者在对性能和代码简洁性有极高要求的场景下,自行构建上层逻辑。

(2)async_simple 阿里巴巴开源的一个轻量级、高性能的 C++ 异步编程框架。已经在生产环境中经过了大量验证,支持 C++20 协程,已经提供类似 folly::FutureFuturePromise 实现。以及线程池、调度器、Lazy 惰性计算等功能。适合高性能、高并发的网络服务、后端服务。

(3)Boost.Asio (C++20 Coroutines): C++ 领域最成熟、功能最强大的网络编程库之一。从 1.70 版本开始深度集成 C++20 协程,提供了 use_awaitable 完成令牌,所有 Asio 的异步操作都可以通过 co_await 来使用。

优势

  • 覆盖网络、定时器、文件 I/O 等几乎所有异步操作,功能极其全面
  • 代码质量和稳定性极高。
  • 丰富、齐全的文档资源。
  • 与现有 Asio 代码无缝集成
  • 适合高性能网络服务器、客户端、分布式系统。

其他优秀的第三方库:

  • folly::coro (Facebook Folly):Facebook Folly 库中的协程实现,为 Facebook 内部的大规模服务提供支持。
  • libunifex (Facebook):一个实验性的库式。
  • co_async:一个轻量级的高并发异步 I/O 库,提供协程支持,适合学习。
  • luncliff/coroutine:协程辅助工具和示例,可以作为学习和实验的资源。

六、结语

C++20 协程彻底改变了现代 C++ 的异步范式。把异步代码同步化,解决回调地狱、阻塞等待和复杂状态管理等痛点。凭借低开销的上下文切换和强大的组合性,协程已经成为构建高性能、高并发和 I/O 密集型应用的基础。

虽然协程学习起来比较不容易,但长远来看,所带来的代码质量和开发效率的提升是巨大的。未来 C++26 std::execution 到来,C++ 异步编程必定更加强大和易用,协程毫无疑问必是核心。

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐