欢迎各位来到今天的讲座。我们今天将深入探讨C++标准库中的异步编程基石之一:std::future。std::future自C++11引入以来,为我们提供了获取异步操作结果的便捷方式。然而,在追求极致性能和灵活性的现代高性能异步框架中,我们却鲜少看到它们直接依赖std::future。相反,这些框架往往倾向于采用回调、自定义的Promise/Future实现,乃至最新的C++20协程机制。
这并非偶然,其背后隐藏着std::future在设计理念和实现细节上的局限性,使得它在面对复杂异步场景和高性能需求时显得力不从心。本次讲座,我将以一名编程专家的视角,为大家剖析std::future的这些局限,并深入探讨高性能异步框架为何会做出这样的选择,以及它们所采用的替代方案的设计哲学与实现精髓。
std::future 的基础回顾与设计哲学
在深入探讨其局限性之前,我们首先回顾一下std::future的基本概念和它旨在解决的问题。
std::future 是什么?
std::future 是一个类型模板,它提供了一种机制来访问某个异步操作的结果。简单来说,当你启动一个可能需要一段时间才能完成的任务时,你可以立即得到一个std::future对象。这个std::future对象就像一张“收据”或“承诺”,它承诺最终会为你提供该任务的计算结果或在计算过程中抛出的异常。
std::future 本身不执行计算,它只是一个结果的持有者。实际的计算通常由以下几种方式启动:
std::async: 这是最简单也最常见的启动异步任务并获取std::future的方式。std::async可以创建一个新线程来执行任务,也可以在当前线程“懒惰”地执行(取决于启动策略)。std::promise:std::promise允许你手动设置一个结果或异常到与之关联的std::future中。它通常用于将某个线程或任务的结果传递给另一个线程。std::packaged_task:std::packaged_task将一个可调用对象包装起来,使其能够异步执行,并自动将结果或异常传递给其关联的std::future。它通常与std::thread或线程池结合使用。
std::future 的基本用法
无论通过哪种方式获取std::future,其基本操作都是类似的:
get(): 阻塞当前线程,直到异步操作完成并返回结果。如果操作抛出异常,get()也会重新抛出该异常。get()方法是单次消费的,一旦调用,其关联的值就被提取,不能再次调用。wait(): 阻塞当前线程,直到异步操作完成,但不获取结果。wait_for()/wait_until(): 带有超时机制的等待,允许在指定时间后返回,无论操作是否完成。
让我们看一个简单的std::async结合std::future的例子:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 一个模拟耗时计算的函数
int calculate_sum(int a, int b) {
std::cout << "Calculating sum of " << a << " and " << b << " in thread: "
<< std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时
if (a < 0 || b < 0) {
throw std::runtime_error("Negative numbers not allowed!");
}
return a + b;
}
int main() {
std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
// 1. 使用 std::async 启动异步任务
std::future<int> future_result_1 = std::async(std::launch::async, calculate_sum, 10, 20);
std::future<int> future_result_2 = std::async(std::launch::async, calculate_sum, 5, -3); // 期望抛出异常
std::cout << "Main thread continues its work..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 主线程做一些其他事情
// 2. 获取第一个结果
try {
int sum1 = future_result_1.get(); // 阻塞直到计算完成
std::cout << "Result 1: " << sum1 << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error getting result 1: " << e.what() << std::endl;
}
// 3. 获取第二个结果 (期望异常)
try {
int sum2 = future_result_2.get(); // 阻塞直到计算完成或异常抛出
std::cout << "Result 2: " << sum2 << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error getting result 2: " << e.what() << std::endl;
}
// 4. 使用 std::promise 和 std::packaged_task
std::promise<std::string> promise_obj;
std::future<std::string> future_promise_result = promise_obj.get_future();
std::thread t1([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Promise thread setting result." << std::endl;
promise_obj.set_value("Hello from promise!");
});
std::cout << "Waiting for promise result..." << std::endl;
std::string promise_val = future_promise_result.get();
std::cout << "Promise result: " << promise_val << std::endl;
t1.join();
std::packaged_task<double(double, double)> task([](double x, double y){
std::this_thread::sleep_for(std::chrono::milliseconds(800));
std::cout << "Packaged task calculating: " << x << " * " << y << std::endl;
return x * y;
});
std::future<double> future_task_result = task.get_future();
std::thread t2(std::move(task), 3.5, 2.0);
std::cout << "Waiting for packaged task result..." << std::endl;
double task_val = future_task_result.get();
std::cout << "Packaged task result: " << task_val << std::endl;
t2.join();
return 0;
}
从上述代码中,我们可以看到std::future提供了一个清晰的抽象:一个异步操作的结果句柄。它的设计哲学在于简单性和一次性结果传递。它旨在为相对独立的异步任务提供一个获取结果的标准方式,避免手动管理复杂的锁和条件变量。
std::future 的核心局限性:为什么它不够“高性能”或“灵活”
尽管std::future在概念上简洁明了,但在构建复杂、高性能的异步系统时,它的局限性就凸显出来了。以下是几个关键方面:
1. 单次消费(Single-shot)的特性
std::future 的 get() 方法是一个单次消费(read-once)操作。一旦你调用了 get(),无论是否成功获取了结果或抛出了异常,这个 std::future 对象的状态都会变为“已检索”(retrieved),之后再次调用 get() 会导致未定义行为(通常是抛出 std::future_error 异常,错误码为 future_already_retrieved)。
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int compute_value() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}
int main() {
std::future<int> f = std::async(compute_value);
int val1 = f.get(); // 第一次获取,成功
std::cout << "First get: " << val1 << std::endl;
try {
int val2 = f.get(); // 第二次获取,将抛出异常
std::cout << "Second get (should not happen): " << val2 << std::endl;
} catch (const std::future_error& e) {
std::cerr << "Caught exception: " << e.what()
<< " with error code: " << static_cast<int>(e.code().value()) << std::endl;
}
// 如果需要多次访问结果,必须使用 std::shared_future
std::future<int> f_shared_source = std::async(compute_value);
std::shared_future<int> sf = f_shared_source.share(); // 将 future 转换为 shared_future
// std::shared_future 可以多次获取结果
int s_val1 = sf.get();
std::cout << "Shared future first get: " << s_val1 << std::endl;
int s_val2 = sf.get(); // 成功
std::cout << "Shared future second get: " << s_val2 << std::endl;
// 原始的 std::future 在 share() 后是无效的,再次 get() 会报错
try {
f_shared_source.get();
} catch (const std::future_error& e) {
std::cerr << "Caught exception from original future after share(): " << e.what() << std::endl;
}
return 0;
}
这种单次消费的特性使得 std::future 在需要将一个异步操作的结果分发给多个消费者时变得非常不便。虽然有 std::shared_future 可以解决多消费者问题,但 std::shared_future 仍然缺乏组合能力,我们稍后会讨论。在很多异步编程场景中,一个操作的结果可能需要被多个后续操作所依赖,或者被多个观察者同时监听,单次消费的限制就成为了一个障碍。
2. 缺乏组合性(Lack of Compositionality)
这是 std::future 最显著的局限性之一。现代异步编程框架的核心需求是能够优雅地组合多个异步操作,形成复杂的异步工作流。例如:
- 链式操作 (
then): 当一个异步操作完成后,执行另一个操作,并将前一个操作的结果作为参数。 - 全部完成 (
when_all): 等待多个异步操作全部完成后,再执行一个操作,并获取所有结果。 - 任意完成 (
when_any): 等待多个异步操作中任意一个完成后,再执行一个操作,并获取完成的那个结果。 - 错误恢复 (
catch/recover): 当异步操作失败时,提供一个备用方案或错误处理逻辑。
std::future 标准库中并没有提供这些高级的组合原语。如果你想实现这些功能,你需要手动使用 get() 或 wait() 配合 std::thread、std::condition_variable 和 std::mutex 来进行复杂的同步,这不仅繁琐,而且容易出错,并且会带来阻塞问题。
考虑一个场景:我们有两个异步任务 taskA 和 taskB,taskB 依赖于 taskA 的结果。使用 std::future,你可能不得不这样做:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int taskA() {
std::cout << "Task A started..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Task A finished." << std::endl;
return 10;
}
int taskB(int input) {
std::cout << "Task B started with input: " << input << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Task B finished." << std::endl;
return input * 2;
}
int main() {
std::cout << "Main thread started." << std::endl;
std::future<int> future_A = std::async(std::launch::async, taskA);
// 如何让 taskB 在 future_A 完成后执行?
// 最直接但效率低下的方法是阻塞主线程:
int result_A = future_A.get(); // 阻塞!
std::cout << "Main thread got result A: " << result_A << std::endl;
std::future<int> future_B = std::async(std::launch::async, taskB, result_A);
int result_B = future_B.get(); // 再次阻塞!
std::cout << "Main thread got result B: " << result_B << std::endl;
// 如果想要并行执行 taskA 和 taskC,然后等待它们全部完成:
std::future<int> future_C = std::async(std::launch::async, []{
std::cout << "Task C started..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "Task C finished." << std::endl;
return 5;
});
// 无法直接组合,只能分别 get(),并且是阻塞的
// 这不是 when_all,而是顺序等待
int result_A_again = std::async(std::launch::async, taskA).get(); // 又一个阻塞
int result_C = future_C.get(); // 阻塞
std::cout << "All parallel tasks (simulated) completed. Result A: "
<< result_A_again << ", Result C: " << result_C << std::endl;
std::cout << "Main thread finished." << std::endl;
return 0;
}
上述代码中,为了实现依赖关系和等待多个任务,我们不得不频繁地调用 get(),这导致了主线程的多次阻塞。这在Web服务器、GUI应用或任何I/O密集型应用中是不可接受的,因为它会耗尽线程池或导致UI冻结。
3. 阻塞式获取结果(Blocking get() and wait())
std::future 的核心问题在于,其主要的取值方式 get() 和 wait() 都是阻塞的。虽然它们可以用于等待一个异步操作完成,但这种阻塞行为在高性能、事件驱动的异步编程模型中是致命的。
在许多高性能异步框架中,例如网络服务器或数据库客户端,通常采用事件循环(event loop)或反应器(reactor)/前摄器(proactor)模式。这意味着一个或少数几个线程负责处理所有的I/O事件和任务调度,而不是为每个任务创建新线程。如果这些事件循环线程调用了 std::future::get() 或 wait(),它们就会被阻塞,无法处理其他I/O事件,导致整个系统的吞吐量急剧下降,甚至出现死锁。
考虑一个Web服务器,一个请求进来,需要调用两个外部服务(都是异步操作)。如果每个外部服务的结果都通过 std::future 来获取,那么服务器处理请求的线程就必须等待,而不是去处理其他并发请求。
// 伪代码:一个简化的服务器请求处理
void handle_request(HttpRequest req) {
// 假设 serviceA 和 serviceB 是异步的,返回 std::future
std::future<ResponseA> future_resA = call_external_serviceA(req.param1);
std::future<ResponseB> future_resB = call_external_serviceB(req.param2);
// 此时,如果服务器是单线程事件循环模式,get() 将阻塞整个事件循环
// 无法处理其他并发请求
ResponseA resA = future_resA.get(); // 阻塞!
ResponseB resB = future_resB.get(); // 再次阻塞!
// 合并结果并发送响应
HttpResponse final_response = combine_results(resA, resB);
send_response(req, final_response);
}
这种阻塞行为与现代异步编程范式的核心理念——非阻塞(non-blocking)和并发(concurrency)而非并行(parallelism)——是背道而驰的。我们希望任务的完成能够通知我们,而不是我们主动去等待它。
4. 线程模型耦合与调度器缺失
std::future 本身并没有内置的调度器抽象。std::async 在默认情况下可能会创建新线程,这对于控制并发度、避免线程上下文切换开销和集成到现有线程池中是不利的。如果你想将 std::promise 或 std::packaged_task 与自己的线程池或事件循环结合,你需要手动管理线程的创建、销毁和任务提交。
std::async 的 std::launch::deferred 策略更是“懒惰”执行,直到 get() 被调用才在调用者线程执行,这可能与你期望的异步行为不符。
高性能异步框架通常需要精细控制任务在哪个线程(或线程池)上执行,以及后续的延续(continuation)在何时何地执行。std::future 没有提供这种控制能力,使得它很难与复杂的调度策略集成。例如,一个网络I/O操作的完成回调,我们希望它在I/O线程上执行,以避免不必要的线程切换。但如果这个回调又触发了一个CPU密集型任务,我们可能希望它被调度到CPU工作线程池中执行。std::future 无法表达这种调度意图。
5. 缺乏取消机制(Lack of Cancellation)
std::future 没有提供一个标准化的机制来取消一个正在进行中的异步操作。一旦 std::async 或 std::packaged_task 被启动,你就很难在外部停止它(除非在任务内部手动检查一个取消标志)。在许多实时或资源受限的场景中,任务取消是一个非常重要的功能,例如:
- 用户关闭了应用或取消了某个操作。
- 某个操作超时,不再需要其结果。
- 资源耗尽,需要停止不必要的计算。
缺乏取消机制意味着一旦任务启动,它就必须运行到完成或出错,这可能浪费宝贵的计算资源。
6. 错误处理的局限性
std::future 通过 get() 方法重新抛出异步任务中发生的异常,这是一种有效的错误传播机制。然而,它仍然是阻塞的,并且不如 Promise-style 的 catch() 或 recover() 方法那样灵活。在 Promise 链中,你可以为链中的每个环节指定不同的错误处理逻辑,或者在某个环节捕获错误后尝试恢复,然后继续后续的成功路径。std::future 无法直接提供这种细粒度的错误流控制。
7. 调试困难
异步编程本身就比同步编程更难调试,因为执行流是非线性的,而且涉及多个线程。std::future 的阻塞行为虽然可能简化某些简单的调试场景(因为可以等待),但在复杂的异步链中,缺乏组合性会使得问题追踪变得更加困难。当你有一个复杂的异步图时,很难知道哪个 get() 调用会阻塞,或者哪个任务会失败。
高性能异步框架的替代方案:回调与自定义 Promise/Future
面对std::future的这些局限,高性能异步框架发展出了自己的解决方案,主要可以分为两大类:回调(Callbacks)和自定义 Promise/Future(或称作 Task/Future),以及C++20后兴起的协程(Coroutines)。
1. 回调函数(Callbacks)
回调函数是异步编程最原始、最直接的形式。其核心思想是,当一个异步操作完成时,它不返回结果,而是调用你提供的一个函数(即回调函数),并将结果或错误作为参数传递给它。
优点:
- 效率高: 回调通常只是一个函数指针或Lambda表达式,开销极小。它直接指定了操作完成后要执行的代码,避免了额外的对象分配和管理。
- 非阻塞: 异步操作在后台进行,一旦完成就触发回调,调用者线程无需阻塞。
- 控制力强: 开发者可以精确控制回调的执行上下文(例如在哪个线程或事件循环上执行)。
缺点:
- 回调地狱(Callback Hell): 当有多个相互依赖的异步操作时,代码会变得高度嵌套,难以阅读、理解和维护。
- 错误处理复杂: 异常传播需要手动管理,通常通过回调函数的额外参数来传递错误对象,而不是通过语言机制(如
try-catch)。 - 缺乏组合性: 很难将多个回调组合成复杂的控制流(例如
when_all)。需要手动编写复杂的逻辑来同步多个回调的完成。 - 生命周期管理: 确保回调函数引用的对象在回调执行时仍然有效是一个挑战。
回调地狱示例:
#include <iostream>
#include <thread>
#include <functional>
#include <chrono>
// 模拟异步操作:从文件读取数据
void readFileAsync(const std::string& filename,
std::function<void(const std::string& data, const std::string& error)> callback) {
std::cout << "Reading file: " << filename << std::endl;
std::thread([=]() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟I/O延迟
if (filename == "error.txt") {
callback("", "Failed to read file: " + filename);
} else {
callback("Content of " + filename, "");
}
}).detach(); // 使用 detach 简化示例,实际生产环境需 join 或管理线程生命周期
}
// 模拟异步操作:处理数据
void processDataAsync(const std::string& data,
std::function<void(const std::string& processedData, const std::string& error)> callback) {
std::cout << "Processing data: " << data.substr(0, 10) << "..." << std::endl;
std::thread([=]() {
std::this_thread::sleep_for(std::chrono::milliseconds(800)); // 模拟CPU计算
if (data.empty()) {
callback("", "Cannot process empty data.");
} else {
callback("PROCESSED(" + data + ")", "");
}
}).detach();
}
// 模拟异步操作:写入结果到数据库
void writeToDbAsync(const std::string& result,
std::function<void(bool success, const std::string& error)> callback) {
std::cout << "Writing to DB: " << result.substr(0, 15) << "..." << std::endl;
std::thread([=]() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟DB I/O
if (result.find("FAIL") != std::string::npos) {
callback(false, "DB write failed due to data content.");
} else {
callback(true, "");
}
}).detach();
}
int main() {
std::cout << "Main thread started." << std::endl;
// 经典的回调地狱
readFileAsync("data.txt", [&](const std::string& data, const std::string& readError) {
if (!readError.empty()) {
std::cerr << "Read error: " << readError << std::endl;
return;
}
processDataAsync(data, [&](const std::string& processedData, const std::string& processError) {
if (!processError.empty()) {
std::cerr << "Process error: " << processError << std::endl;
return;
}
writeToDbAsync(processedData, [&](bool success, const std::string& dbError) {
if (!success) {
std::cerr << "DB write error: " << dbError << std::endl;
return;
}
std::cout << "All operations completed successfully! Final result: " << processedData << std::endl;
});
});
});
// 尝试一个错误路径
readFileAsync("error.txt", [&](const std::string& data, const std::string& readError) {
if (!readError.empty()) {
std::cerr << "Read error (error.txt path): " << readError << std::endl;
return;
}
// ... 后续处理
});
std::cout << "Main thread doing other work while async tasks run." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); // 给异步任务留出时间完成
std::cout << "Main thread finished." << std::endl;
return 0;
}
这段代码展示了回调地狱的典型特征:深层嵌套,错误处理分散且重复,逻辑难以追踪。
2. 自定义 Promise/Future (或者 Task/Future)
为了解决回调地狱和std::future的局限性,许多高性能框架(如 Facebook 的 Folly::Future、Seastar::future、Boost.ASIO 的 async_result/awaitable 等)都实现了自己版本的 Promise/Future 模式。这些自定义实现通常在概念上与 JavaScript 的 Promise 相似,但针对 C++ 的性能和资源管理特性进行了优化。
设计原则:
- 非阻塞组合: 提供
then()、when_all()、when_any()、recover()等方法,允许以非阻塞的方式链式组合异步操作。 - 显式调度: 允许开发者指定链中的每个操作(延续)在哪个执行上下文(线程、线程池、事件循环)上运行。
- 统一错误处理: 通过
catch()或recover()方法提供结构化的错误处理机制,使异常能够像同步代码一样沿着链条传播和被处理。 - 取消机制: 常常内置任务取消功能。
- 轻量级: 尽可能减少内存分配和运行时开销,以实现高性能。
自定义 Promise/Future 的概念模型:
一个自定义的 Future 对象通常包含一个指向共享状态的指针。这个共享状态存储了异步操作的结果(或异常)以及一系列等待该结果的延续(continuations)。当操作完成时,它会设置结果到共享状态,并通知或调度所有注册的延续执行。
核心方法:
then(callback): 当当前 Future 完成时,执行callback。callback的结果会成为一个新的 Future。catch(error_callback): 当当前 Future 失败时,执行error_callback。finally(callback): 无论成功或失败,都在 Future 完成后执行callback。when_all(futures...): 返回一个新的 Future,当所有传入的 Future 都完成后,它才会完成。when_any(futures...): 返回一个新的 Future,当任意一个传入的 Future 完成时,它就会完成。
概念性 C++ Promise-like API 示例:
// 假设有一个自定义的 Future 库,例如 MyFramework::Future
// 这是一个高度简化的概念模型,实际实现会复杂得多
namespace MyFramework {
// 前向声明
template<typename T> class Future;
// Promise 用于设置结果
template<typename T>
class Promise {
public:
// ... 构造函数等
void set_value(T value);
void set_exception(std::exception_ptr ex);
Future<T> get_future();
// ...
};
// Future 用于获取结果和组合
template<typename T>
class Future {
public:
// ... 构造函数等
// 核心组合方法:then
template<typename F_Callback>
auto then(F_Callback&& callback) -> Future<decltype(callback(std::declval<T>()))>;
// 错误处理方法:catch
template<typename F_ErrorCallback>
auto catch_error(F_ErrorCallback&& error_callback) -> Future<T>;
// 阻塞获取结果 (通常不鼓励在高性能框架中使用)
T get();
void wait();
// 静态工厂方法
static Future<T> make_ready_future(T value);
static Future<T> make_exceptional_future(std::exception_ptr ex);
// ... 其他方法,如 cancel(), when_all(), when_any()
};
// 异步操作的启动器
template<typename F, typename... Args>
Future<std::invoke_result_t<F, Args...>> async_task(F&& func, Args&&... args) {
MyFramework::Promise<std::invoke_result_t<F, Args...>> p;
MyFramework::Future<std::invoke_result_t<F, Args...>> f = p.get_future();
std::thread([p_move = std::move(p), func_move = std::forward<F>(func), ...args_move = std::forward<Args>(args)]() mutable {
try {
if constexpr (std::is_void_v<std::invoke_result_t<F, Args...>>) {
func_move(args_move...);
p_move.set_value();
} else {
p_move.set_value(func_move(args_move...));
}
} catch (...) {
p_move.set_exception(std::current_exception());
}
}).detach(); // 实际框架会使用线程池或事件循环调度
return f;
}
} // namespace MyFramework
// ------------------- 实际使用示例 -------------------
#include <iostream>
#include <string>
#include <vector>
#include <numeric>
// 模拟异步函数
MyFramework::Future<std::string> async_read_file(const std::string& filename) {
return MyFramework::async_task([filename]() -> std::string {
std::cout << "Async read file: " << filename << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
if (filename == "error.txt") {
throw std::runtime_error("File read error for " + filename);
}
return "Content of " + filename;
});
}
MyFramework::Future<int> async_process_data(const std::string& data) {
return MyFramework::async_task([data]() -> int {
std::cout << "Async process data: " << data.substr(0, 10) << "..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(800));
if (data.find("error") != std::string::npos) {
throw std::runtime_error("Processing failed for data containing 'error'");
}
return static_cast<int>(data.length() * 2);
});
}
MyFramework::Future<void> async_write_to_db(int result) {
return MyFramework::async_task([result]() {
std::cout << "Async write to DB: " << result << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
if (result < 0) {
throw std::runtime_error("Invalid result for DB write: " + std::to_string(result));
}
// 模拟成功写入
});
}
int main() {
std::cout << "Main thread started." << std::endl;
// 链式组合操作,不再有回调地狱
MyFramework::Future<void> final_future =
async_read_file("data.txt")
.then([](const std::string& data) {
std::cout << "Read completed, chaining to process..." << std::endl;
return async_process_data(data); // then 内部可以返回另一个 Future
})
.then([](int processed_val) {
std::cout << "Processing completed, chaining to DB write..." << std::endl;
return async_write_to_db(processed_val);
})
.catch_error([](const std::exception_ptr& ex_ptr) { // 统一错误处理
try {
std::rethrow_exception(ex_ptr);
} catch (const std::exception& e) {
std::cerr << "Caught error in chain: " << e.what() << std::endl;
}
// 返回一个空的 future<void> 以继续链条,或者抛出以中断
return MyFramework::Future<void>::make_exceptional_future(std::current_exception()); // 示例:继续传播异常
});
// 等待整个链条完成 (在实际框架中,这通常在事件循环中完成,而非阻塞主线程)
std::cout << "Main thread waiting for chain to complete..." << std::endl;
final_future.wait(); // 这里仍然是阻塞的,因为我们没有事件循环来驱动延续
std::cout << "n--- Testing error path ---" << std::endl;
MyFramework::Future<void> error_future =
async_read_file("error.txt") // 这个会失败
.then([](const std::string& data) {
std::cout << "This part should not execute on error path." << std::endl;
return async_process_data(data);
})
.catch_error([](const std::exception_ptr& ex_ptr) {
try {
std::rethrow_exception(ex_ptr);
} catch (const std::exception& e) {
std::cerr << "Caught error in error path chain: " << e.what() << std::endl;
}
return MyFramework::Future<void>::make_ready_future(); // 示例:捕获错误后,后续链条继续执行成功路径
})
.then([]() {
std::cout << "After error recovery, this continuation runs." << std::endl;
return MyFramework::Future<void>::make_ready_future();
});
error_future.wait();
// 模拟 when_all
std::cout << "n--- Testing when_all concept ---" << std::endl;
auto f1 = MyFramework::async_task([]{ std::this_thread::sleep_for(std::chrono::seconds(1)); return 1; });
auto f2 = MyFramework::async_task([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); return 2; });
auto f3 = MyFramework::async_task([]{ std::this_thread::sleep_for(std::chrono::seconds(2)); return 3; });
// 假设 MyFramework::when_all 存在
// auto all_futures = MyFramework::when_all(f1, f2, f3);
// all_futures.then([](std::tuple<int, int, int> results) {
// std::cout << "All tasks completed: " << std::get<0>(results) << ", "
// << std::get<1>(results) << ", " << std::get<2>(results) << std::endl;
// }).wait(); // 再次阻塞,实际中应由事件循环驱动
std::cout << "Main thread finished." << std::endl;
return 0;
}
这段代码展示了自定义 Promise/Future 模式如何通过 then() 和 catch_error() 优雅地组合异步操作,解决了回调地狱和错误处理分散的问题。其核心思想是将操作的延续(continuation)注册到 Future 对象上,而不是阻塞等待。当 Future 完成时,这些延续会被调度执行。
3. C++20 协程(Coroutines)
C++20 引入的协程是异步编程领域的一项革命性特性。它提供了一种语言级别的机制来编写暂停和恢复的函数,从而极大地简化了异步代码的编写。协程本身不是一个异步库,而是一个强大的原语(primitive),可以用来构建高效的异步框架,包括自定义的 Promise/Future 实现。
核心概念:
co_await: 用于暂停当前协程,等待一个“可等待对象”(Awaitable)完成。当可等待对象完成后,协程从暂停点恢复。co_yield: 用于暂停当前协程,并返回一个值给调用者(主要用于生成器)。co_return: 用于完成协程并返回一个值。- Awaitable: 任何定义了
await_ready(),await_suspend(),await_resume()方法的对象,都可以被co_await。 - Promise Type: 协程函数返回类型(例如
MyFuture<T>)的伴随类型,它定义了协程的生命周期管理,如如何创建协程帧、如何处理co_return和异常。 std::coroutine_handle: 一个句柄,用于控制协程的恢复和销毁。
协程如何解决 std::future 的局限性:
- 非阻塞:
co_await的核心就是非阻塞。当一个操作尚未完成时,协程会暂停,CPU资源可以被其他任务使用。一旦操作完成,协程会被调度器恢复。 - 顺序化代码: 协程允许你以几乎同步的、顺序化的方式编写异步代码,极大地提高了可读性和可维护性,彻底解决了回调地狱。
- 组合性: 结合自定义的 Awaitable 和 Promise Type,可以轻松实现
when_all,when_any等组合模式。 - 调度器集成: Awaitable 的
await_suspend方法可以接收std::coroutine_handle,从而允许 Awaitable 将协程的恢复任务提交给任意的调度器(线程池、事件循环),实现对执行上下文的精细控制。 - 错误处理: 异常在协程中可以像同步代码一样通过
try-catch传播和处理。 - 取消: 可以在 Awaitable 的
await_suspend中注册取消回调,或通过 Promise Type 实现取消逻辑。
协程示例 (结合 Boost.ASIO 的 awaitable 概念):
虽然 C++ 标准库没有提供 std::async 的 co_await 版本,但像 Boost.ASIO 这样的框架已经将其异步操作包装成了可等待对象,与协程无缝集成。
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/ts/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <thread>
#include <string>
#include <vector>
// 假设我们有一个异步操作,例如从网络读取数据
// 在 Boost.ASIO 中,许多异步操作本身就是 awaitable 的
// 例如:boost::asio::async_read, boost::asio::async_write, boost::asio::steady_timer::async_wait
// 模拟一个异步读取操作,返回一个字符串
boost::asio::awaitable<std::string> async_read_from_source(boost::asio::io_context& io_context, const std::string& source_name) {
std::cout << "Co-routine: Starting async read from " << source_name << " in thread: " << std::this_thread::get_id() << std::endl;
boost::asio::steady_timer timer(io_context, boost::asio::chrono::seconds(1));
co_await timer.async_wait(boost::asio::use_awaitable); // 暂停协程,等待定时器
if (source_name == "error_source") {
throw std::runtime_error("Simulated read error from " + source_name);
}
co_return "Data from " + source_name + " (read at " + std::to_string(std::time(nullptr)) + ")";
}
// 模拟一个异步处理操作
boost::asio::awaitable<int> async_process_data_coro(const std::string& data) {
std::cout << "Co-routine: Processing data: " << data.substr(0, 10) << "..." << " in thread: " << std::this_thread::get_id() << std::endl;
// 假设这是一个CPU密集型任务,我们可能希望它在特定的线程池上运行
// 但在ASIO的awaitable上下文中,它通常在io_context线程上运行,除非显式调度
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟计算
co_return static_cast<int>(data.length() * 3);
}
// 模拟一个异步写入操作
boost::asio::awaitable<void> async_write_to_sink(boost::asio::io_context& io_context, int result) {
std::cout << "Co-routine: Writing result " << result << " to sink in thread: " << std::this_thread::get_id() << std::endl;
boost::asio::steady_timer timer(io_context, boost::asio::chrono::seconds(1));
co_await timer.async_wait(boost::asio::use_awaitable); // 暂停协程,等待定时器
if (result < 0) {
throw std::runtime_error("Invalid result for sink write: " + std::to_string(result));
}
std::cout << "Co-routine: Successfully wrote " << result << std::endl;
co_return;
}
// 主异步流程,使用协程链式调用
boost::asio::awaitable<void> main_async_flow(boost::asio::io_context& io_context) {
std::cout << "Main async flow started in thread: " << std::this_thread::get_id() << std::endl;
try {
std::string data = co_await async_read_from_source(io_context, "source_A");
std::cout << "Co-routine: Received data: " << data << std::endl;
int processed_val = co_await async_process_data_coro(data);
std::cout << "Co-routine: Processed value: " << processed_val << std::endl;
co_await async_write_to_sink(io_context, processed_val);
std::cout << "Co-routine: All operations in main flow completed successfully!" << std::endl;
} catch (const std::exception& e) {
std::cerr << "Co-routine: Caught exception in main flow: " << e.what() << std::endl;
}
co_return;
}
// 演示 when_all 概念
boost::asio::awaitable<void> all_tasks_flow(boost::asio::io_context& io_context) {
std::cout << "nCo-routine: Starting all_tasks_flow in thread: " << std::this_thread::get_id() << std::endl;
try {
auto f1 = async_read_from_source(io_context, "source_X");
auto f2 = async_read_from_source(io_context, "source_Y");
auto f3 = async_read_from_source(io_context, "source_Z");
// 使用 std::tuple 和 co_await 来等待所有 Future (Boost.ASIO 提供了 when_all 类似的机制)
auto [res1, res2, res3] = co_await (boost::asio::when_all(std::move(f1), std::move(f2), std::move(f3)));
std::cout << "Co-routine: All parallel reads completed. Results: "
<< std::get<0>(res1) << ", "
<< std::get<0>(res2) << ", "
<< std::get<0>(res3) << std::endl;
} catch (const std::exception& e) {
std::cerr << "Co-routine: Caught exception in all_tasks_flow: " << e.what() << std::endl;
}
co_return;
}
int main() {
std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
boost::asio::io_context io_context;
// 启动主异步流程协程
boost::asio::co_spawn(io_context, main_async_flow(io_context), boost::asio::detached);
// 启动另一个协程演示 when_all
boost::asio::co_spawn(io_context, all_tasks_flow(io_context), boost::asio::detached);
std::cout << "Main thread running io_context..." << std::endl;
io_context.run(); // 运行事件循环,驱动协程执行
std::cout << "Main thread finished." << std::endl;
return 0;
}
通过 co_await,异步操作看起来就像同步函数调用一样,极大地提高了代码的可读性,并消除了回调地狱。协程在底层通过状态机和 std::coroutine_handle 管理延续,使得我们可以以非阻塞的方式构建复杂的异步流。Boost.ASIO 的 co_spawn 和 awaitable 类型完美地展示了如何将协程与事件循环和调度器集成。
Coroutines: C++ 20 异步的终极答案?
C++20 协程的引入,无疑为C++异步编程带来了范式上的转变。它们并非直接取代std::future,而是提供了一个更低层、更强大的机制,使得库开发者可以构建出比std::future更灵活、更高效的异步抽象。
可以说,协程是构建高性能异步框架的理想基石。它们解决了std::future的几乎所有核心局限:
- 非阻塞:
co_await是非阻塞的,它允许在等待异步操作时暂停协程,释放当前线程去执行其他任务。 - 组合性: 通过
co_await多个 awaitable 对象,可以非常自然地实现链式、并行等待等组合模式,无需手动管理回调或复杂的 Promise 链。 - 调度器集成: 协程的 Promise Type 和 Awaitable 能够与自定义的调度器(如事件循环、线程池)深度集成,精确控制协程的暂停和恢复发生在哪里。
- 取消机制: 协程的 Promise Type 和 Awaitable 可以被设计为支持取消,例如在
await_suspend中注册取消回调,或在协程内部检查取消状态。 - 错误处理: 协程内的异常处理与同步代码一致,可以使用
try-catch块捕获和处理异常。 - 可读性: 协程代码看起来像同步代码,极大地提高了异步逻辑的可读性和可维护性,彻底终结了“回调地狱”。
然而,协程也并非没有成本。它们引入了新的概念(Awaitable、Promise Type、协程帧),学习曲线相对陡峭。此外,协程本身不提供调度器,你需要一个像 Boost.ASIO 这样的库来管理协程的执行。
在C++20及以后的世界里,std::future 的角色可能会变得更加聚焦:它仍然是处理简单、一次性、独立异步任务的有效工具,特别是当你可以接受阻塞等待结果时(例如在一个专门的后台线程中)。但对于需要复杂组合、非阻塞I/O、精细调度和高吞吐量的场景,协程与自定义的异步库(如 Boost.ASIO、libunifex 等)的结合将是未来的主流。
设计考量与权衡
| 特性/模式 | std::future |
回调函数 | 自定义 Promise/Future | C++20 协程 |
|---|---|---|---|---|
| 组合性 | 差,无内置 then, when_all 等 |
差,导致回调地狱 | 优秀,通过 then, when_all 等实现 |
优秀,通过 co_await 实现顺序化、可读性高的组合 |
| 阻塞行为 | get() 和 wait() 阻塞 |
非阻塞,通过回调通知 | 非阻塞,通过注册延续通知 | 非阻塞,通过 co_await 暂停和恢复 |
| 调度器集成 | 无内置支持,依赖 std::async 或手动线程管理 |
强,可在任意上下文调用回调 | 良好,通常支持在指定调度器上执行延续 | 优秀,Awaitable 可将恢复任务提交给任意调度器 |
| 错误处理 | get() 重新抛出异常 |
手动传递错误参数,分散且易错 | 统一 catch() 或 recover() |
try-catch 与同步代码一致 |
| 取消机制 | 无标准支持 | 需手动实现 | 通常内置支持 | 可通过 Promise Type 和 Awaitable 实现 |
| 代码可读性 | 简单任务尚可,复杂组合差 | 复杂任务极差(回调地狱) | 良好,链式调用提升可读性 | 极佳,异步代码形如同步代码 |
| 运行时开销 | 适中(通常涉及线程创建或同步对象) | 最低(函数调用开销) | 适中(共享状态对象、延续对象的分配) | 较低(协程帧分配,但无栈) |
| 学习曲线 | 低 | 低(但组合复杂时陡峭) | 中到高 | 高(概念新颖且底层) |
选择哪种异步编程范式,始终是工程上的权衡。
std::future: 适用于简单的、一次性的、无需复杂组合的异步任务,尤其是在后台线程中阻塞等待结果可以接受的场景。其简单性是优势。- 回调函数: 在对性能要求极高、且异步逻辑相对简单直接的场景(如底层I/O库),或在资源受限的环境中仍有应用。但需要严格控制代码复杂度。
- 自定义 Promise/Future: 对于需要复杂异步流、非阻塞、统一错误处理和良好组合性的应用来说,是比回调更好的选择。许多高性能框架都基于此构建。
- C++20 协程: 是目前C++异步编程最现代、最强大的解决方案。它提供了语言级别的支持,使得构建高性能、高可读性、高灵活性的异步系统成为可能。它是未来高性能异步框架的首选基石。
未来展望
C++ 标准委员会正在积极推进异步编程的标准化工作。std::future 的下一代演进,以及更通用的执行器(Executors)和调度器(Schedulers)抽象,是未来的重要方向。std::execution 提案旨在提供一个统一的框架来定义和组合异步操作,并能够在各种执行上下文(线程池、事件循环、GPU等)上运行。这些未来的标准将与C++20协程相结合,为C++提供一个强大而统一的异步编程模型。
结语
std::future 在 C++ 异步编程的初期扮演了重要角色,为我们打开了异步世界的大门。然而,在追求极致性能和复杂异步工作流的场景下,其单次消费、缺乏组合性、阻塞行为以及调度器缺失等局限性使其难以胜任。高性能异步框架通过发展回调、自定义 Promise/Future 乃至 C++20 协程等机制,有效地克服了这些挑战。理解这些替代方案的设计哲学与优势,对于构建现代、高效的 C++ 应用程序至关重要。