C++20 异步编程:用future、promise 还是协程?
C++20引入协程为异步编程带来革新,相比C++11的future/promise机制具有明显优势。future/promise虽简化了异步任务管理,但存在组合性差、阻塞调用、回调地狱等问题。协程通过"暂停-恢复"机制实现更直观的异步代码编写,降低内存开销和上下文切换成本。C++20提供co_await、co_yield等关键字支持协程,配合第三方库可构建高效异步应用。协程代表
C++20 异步编程:用`future`/`promise` 还是协程?
一、前言
网络编程的程序并发处理能力、系统资源的利用率非常重要。不管是构建高性能的网络服务器、响应迅速的桌面应用,还是处理海量数据的批处理系统,都要异步编程。异步是指在等待耗时操作完成的同时,继续执行其他任务,避免界面卡顿、提高吞吐量,充分使用多核处理器资源。
C++ 作为系统级编程语言,一生都在追求性能和控制力,异步编程也在不断演进。从最开始的基于裸线程和回调函数的手动管理,到 C++11 引入的 std::thread、std::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::future 和 std::promise 非常强大,简化并发编程中异步任务的结果传递和同步问题。提供比裸线程和条件变量更高级、更易用的抽象。
相关概念:
std::promise:异步操作的“写入端”。std::promise对象存储一个值或一个异常,在某个时刻被异步的提供。提供有set_value()方法可以设置结果值,以及set_exception()方法设置一个异常。只要设置了值或异常,std::promise就变成“已完成”。每个std::promise对象都关联一个std::future对象,通过get_future()方法可以获取到这个关联的std::future。std::future:异步操作的“读取端”。std::future对象是std::promise对象的“读取端”。代表一个可能在未来某个时间点可用的结果。通过std::future的get()方法,可以阻塞当前线程直到关联的std::promise设置了结果或异常,然后获取到该结果或重新抛出异常。std::future还提供了wait()和wait_for()等非阻塞等待方法,检查结果是否可用。std::async:封装了线程创建、任务执行以及std::promise和std::future的管理。可以以函数调用的方式启动一个异步任务,并直接返回一个std::future对象,不需要手动创建std::promise和std::thread。std::async默认会根据系统负载选择是立即执行任务(std::launch::deferred)还是在新线程中异步执行(std::launch::async)。
优点:
- 对于简单的异步任务,
std::future和std::promise是一种清晰且易于理解的模式:一个生产者(promise)提供结果,一个消费者(future)获取结果。结合std::async,非常简洁。 std::future和std::promise不需要任何第三方库即可使用。- 适合简单的、独立的异步任务。 任务是独立的,结果只需要获取一次,且后续没有复杂的依赖或链式操作时,
std::future/std::promise就非常的合适。
缺点:
- 不能清晰的表达复杂的异步流程(无
.then()链式调用),这是std::future/std::promise最主要的局限。没有提供类似 JavaScript Promise 或其他异步框架中的.then()方法,无法直接把多个异步操作串联起来形成一个链式调用。如果一个异步任务的结果是另一个异步任务的输入,或者要等待多个异步任务都完成后才能继续,就不得不引入额外的同步机制(std::packaged_task、std::shared_future,或者手动管理线程和条件变量),这样一来代码就变得复杂了。 std::future::get()方法是阻塞的。调用get()时结果还没有准备好,当前线程就被暂停执行,直到结果可用。这在同时处理多个异步操作或保持 UI 响应性的场景下是不能接受的。虽然有wait()和wait_for(),但要手动轮询或超时处理。std::future的get()方法只能被调用一次。一旦调用,std::future对象就变为“无效”的,不能再次获取结果。虽然std::shared_future可以解决多次获取结果的问题,但还是不能解决组合性差和阻塞性问题。- 为了避免
get()的阻塞,会去用回调函数。在没有语言层面支持的情况下,手动管理回调会导致层层嵌套的“回调地狱”。或者,为了实现复杂的依赖关系,不得不编写大量的互斥量、条件变量等同步原语,增加代码的复杂性。
适用场景:
- 启动一个独立的、无需复杂后续操作的后台任务。
- 作为协程内部或更高级抽象的底层构建块。
三、C++20 协程 (Coroutines)
C++20 引入的协程(Coroutines)是语言层面的一次重大革新,为异步编程带来革命性的改变。协程是编写非阻塞、并发代码的新范式,让异步逻辑的表达更加直观、可读,能解决异步编程常见的“回调地狱”和复杂同步机制的问题。
协程是一种用户态的轻量级并发执行单元。跟线程不同,协程的调度完全由程序员控制,而不是由操作系统内核控制。一个协程可以在执行过程中暂停(suspend),控制权交还给调用者或调度器,然后在某个时刻从暂停点恢复(resume)执行。这种“暂停-恢复”的能力是协程的核心特征。协程是“栈无关”的,没有自己的独立栈空间,而是用调用者的栈空间,这可以大大降低内存开销和上下文切换的成本。
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 在组合性和非阻塞性方面的不足:
co_await可以像编写同步代码一样,串联多个异步操作。一个异步操作的结果可以直接作为下一个异步操作的输入,不再需要嵌套回调或复杂的同步原语。- 协程遇到
co_await表达式时,如果被等待的异步操作尚未完成,协程会立即暂停执行,把控制权交还给调用者或事件循环。当前线程不会被阻塞,可以继续执行其他任务。当异步操作完成后,协程会被恢复,从co_await点继续执行。 - 协程的异步代码看起来和写起来都像同步代码。这种“线性化”的编程风格消除了回调函数的层层嵌套。
- 协程可以在函数体的任意位置暂停和恢复,比线程更细粒度的控制能力。
协程的优势:
- 异步逻辑以顺序方式表达。
- 栈无关,上下文切换开销远小于线程,且可以创建大量的协程而不会耗尽系统资源。一个线程可以同时管理成千上万个协程。
- 在函数内部进行非阻塞的等待和控制流的转移。
- 减少操作系统层面的线程调度和上下文切换,带来更好的性能。
协程很好,但是,也不可避免的出现一些缺点:
- C++20 协程的底层机制比较复杂,学习起来比较困难,要一定时间去理解工作原理。
- C++20 标准只提供协程的底层语言原语,没有直接提供高级的异步框架。
- 协程的执行是非线性的,堆栈跟踪不能直观反映协程的完整调用链,调试起来会比较的复杂。
- 相比其他语言(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_type、awaitable、awaiter 等底层概念,以及协程的生命周期管理。 |
| 标准库支持度 | 完全由标准库提供,开箱即用。 | 只提供语言层面的底层原语,缺乏高级的协程类型、调度器和异步 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_return 、 std::coroutine_handle ),还没有提供开箱即用的高级组件。
- 标准库没有提供像
task<T>这样的通用协程返回类型。要自己实现或使用库提供的promise_type。 - 协程要一个机制来调度执行,特别是在
co_await暂停后恢复时。标准库还没有提供通用的执行器或调度器。 - 网络、文件等异步 I/O 操作是异步编程的核心,但标准库没有提供协程友好的异步 I/O 接口。
- 协程间的同步也要协程友好的版本,避免阻塞调度器。
- 虽然协程支持异常传播,但更高级的错误处理和任务取消机制还要库来提供。
所以,第三方库来填补这些空白,提供完整的协程生态系统。
当前 C++ 协程领域主流协程库:
(1)cppcoro: 由 C++ 协程提案的主要作者之一 Lewis Baker 主导开发的库。提供一套设计优雅、基础且高效的协程类型和辅助工具。是协程设计的“事实标准”和学习协程底层机制的优秀参考。只包含核心的协程抽象,不绑定特定的 I/O 模型或调度器。提供 task<T>、generator<T>、async_mutex、async_manual_reset_event 等基础协程类型和同步原语。适合作为其他协程库或框架的基础,或者在对性能和代码简洁性有极高要求的场景下,自行构建上层逻辑。
(2)async_simple: 阿里巴巴开源的一个轻量级、高性能的 C++ 异步编程框架。已经在生产环境中经过了大量验证,支持 C++20 协程,已经提供类似 folly::Future 的 Future 和 Promise 实现。以及线程池、调度器、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++ 异步编程必定更加强大和易用,协程毫无疑问必是核心。

更多推荐



所有评论(0)