今天我们来介绍一种异步与并发编程的解决方案future和promise,并将结合C++,了解C++中如何使用这样的异步与并发编程模型。
历史简介
本段引用自 https://chuquan.me/2022/12/05/future-and-promise/
1962. Thunk
关于 Future 和 Promise 的起源,最早可以追溯到 1961 年的 Thunk。根据创造者 P.Z. Ingerman 的描述,Thunk 是提供地址的一段代码。
Thunk 被设计为一种将实际参数绑定到 Algol-60 过程调用中的正式定义的方法。如果用表达式代替形式参数调用过程,编译器会生成一个 thunk,它将执行表达式并将结果的地址留在某个标准位置。
1977. Future
1977 年,Henry C. Baker 和 Hewitt 在论文《The Incremental Garbage Collection of Process》中首次提到 Future。
他们提出了一个新的术语 call-by-future,用于描述一种基于 Future 的调用形式。当将表达式提供给执行器时,将返回该表达式的 .future。如果表达式返回类型为值类型,那么当未来表达式计算得到值时,会将值返回。这里会为每一个 future 都会创建一个进程,并立即执行表达式。如果表达式已完成,则值立即可用;如果表达式未完成,则请求进程等待表达式执行完成。
在论文中,Future 主要由三部分组成:
进程(Process):用于执行表达式的进程。
单元(Cell):可写入值的内存地址,用于存储表达式的未来值。
队列(Queue):等待未来值的进程列表。
从 Future 的概念我们可以看出,论文所提到的 Future 几乎已经和现代的 Future 概念非常接近了。
1985. Multilisp
1985 年,Robert H. Halstead 在论文《Multilisp: A Language for Concurrent Symbolic Computation》中提出的 Multilisp 语言支持了基于 future 注解的 call-by-future 能力。
在 Multilisp 中,如果变量绑定到 Future 的表达式,则会自动创建一个新的进程。表达式会在新的进程中执行,一旦执行完成,则将计算结果保存至变量引用中。通过这种方式,Multilisp 支持在新进程中同时计算任意表达式的能力。因此,也支持无需等待 Future 完成,继续执行其他计算的能力。这样的话,如果 Future 的值从未使用过,那么整个进程就不会被阻塞,从而消除了潜在的死锁源。
相比于 1977 年提出的 Future,Mutilisp 实现的 Future 支持在特定情况下不阻塞进程,从而一定程度上优化了程序的执行效率。
1988. Promise
1988 年,Liskov 和 Shrira 在论文《Distributed Programming in Argus》中提出的 Argus 语言设计了一种称为 Promise 的结构。
与 Multilisp 中的 Future 类似,Argus 中的 Promise 也提供一个用于存储未来值的占位符。Promise 的特别之处在于,当调用 Promise 时,会立即创建并返回一个 Promise,并在新进程中进行类型安全的异步 PRC 调用。当异步 PRC 调用执行完毕,由调用者设置返回值。
设计思想
Future和Promise可以视为同一个异步编程技术的两个部分。
Futrue:直译为"未来",这个值在未来会被用到,也就是表示异步任务的返回值,提前占位了一个未来值,该值将被消费者消费;
Promise:直译为"承诺",它表示了一个未来值的生产过程,“承诺会”生产出某个值,作为Futrue,实际上是一个值的生产者。
C++中的相关API
参考 https://zh-blog.logan.tw/2021/09/26/cxx-thread-promise-future-packaged-task-async-usage/
在C++中,相关的接口被如下组织:

promise & future
要在C++中使用该组异步接口,首先需要定义一个promise结构体,它表示某一个数据的输入端。
//头文件#include <future>
//定义一个promise,传递int类型的未来值:std::promise<int> p;//定义一个future,并于p绑定(直接调用了p的接口获取)std::future<int> f = p.get_future();当某个值准备好后,调用promise的set_value接口,设置好未来值,随后,就能使用future的接口将该值读取出来。
#include <future>#include <iostream>int main() { std::promise<int> p; std::future<int> f = p.get_future(); p.set_value(42); // 这个值对p而言是只写的 std::cout << f.get() << std::endl; // 这个值对f而言是只读的}假设我们将p传入到某一个线程,在这个线程内部,调用p的set_value接口,在另一个线程内部,调用f的get接口,就可以实现线程间的异步同步处理。
#include <future>#include <iostream>#include <thread>int main() { std::promise<int> p; std::future<int> f = p.get_future(); std::thread t( [](std::promise<int> p) { p.set_value(42); }, std::move(p)); std::cout << f.get() << std::endl; t.join();}需要注意的是,对应的一套 p.set_value 和 f.get 只能被使用一次,否则会抛出future_error的exception。
wait, wait_for, wait_until
由于异步之间效率的不同,某一个未来值可能迟迟不出现,这就会出现另一个线程的阻塞等待,C++也提供了相应的接口,并给出了超时语义。
void wait():阻塞式的等待,直到future的值可以被读取;
future_status wait_for(const std::chrono::duration<...> &):等待存在最大时间限制,返回future_status;
future_status_wait_until(const std::chrono::time_point<...> &):等待到某个时间,返回future_status。
future_status可以有以下取值:
future_status | 定义 |
deferred | 这是一个惰性估值(async中会使用) |
ready | 已经可以读取值 |
timeout | 等待超时 |
关于wait相关的例子就不在此细说,有兴趣的读者可以自行尝试。
shared_future
std::future 是不可拷贝的,不是线程安全的,只能通过移动(std::move) 语义传递。只能由单一消费者消费结果,如果存在对一个future的多个线程同时访问需求,需要使用到shared_futrue。
shared_future 支持共享访问,允许多个线程共享访问异步操作的结果,它允许被拷贝,每个线程都可以对不同的shared_future拷贝获取相同的结果。
#include <iostream>#include <future>#include <thread>
// 模拟一个耗时计算的函数int compute() { return 42;}
int main() { // 创建一个 std::promise 和与之关联的 std::future std::promise<int> promise; std::future<int> fut = promise.get_future();
// 将 std::future 转换为 std::shared_future std::shared_future<int> shared_fut = fut.share();
// 在另一个线程中设置 promise 的值 std::thread t([&promise]() { int result = compute(); // 计算结果 promise.set_value(result); // 将结果传递给 promise });
// 多个地方获取结果 std::cout << "Thread 1: " << shared_fut.get() << std::endl; std::cout << "Thread 2: " << shared_fut.get() << std::endl;
// 共享的 future 可以拷贝 std::shared_future<int> shared_fut_copy = shared_fut; std::cout << "Thread 3 (copy): " << shared_fut_copy.get() << std::endl;
t.join(); // 等待线程结束 return 0;}
output:Thread 1: 42Thread 2: 42Thread 3 (copy): 42packaged_task
在上面的例子中,我们总是希望在某个方法中异步的完成它的值,因此,我们需要设计出这个方法,packaged_task类试图将promise和实现promise的方法进一步封装为一个完整的结构。
#include <future>#include <iostream>int compute(int a, int b) { return 42 + a + b;}int main() { // 相当于定义了promise 并绑定了对应的方法 std::packaged_task<int(int, int)> task(compute); std::future<int> f = task.get_future(); // task内部重载了(),将直接调用compute task(3, 4); std::cout << f.get() << std::endl; return 0;}简化的 packaged_task 实现如下:
#include <exception>#include <functional>#include <future>template <typename Func>class my_packaged_task;template <typename Ret, typename... Args>class my_packaged_task<Ret(Args...)> {private: std::promise<Ret> promise_; std::function<Ret(Args...)> func_;public: my_packaged_task(std::function<Ret(Args...)> func) : func_(std::move(func)) {} // 重载 () 操作 void operator()(Args&&... args) { try { promise_.set_value(func_(std::forward<Args&&>(args)...)); } catch (...) { promise_.set_exception(std::current_exception()); } } std::future<Ret> get_future() { return promise_.get_future(); }};因此,promise被进一步封装,对外不再可见,当需要调用预设的方法为future赋值时,只需要简单的调用task重载后的函数即可。
async
在将promise封装为packaged_task后,我们依然有大量的需求是创建一个线程,去执行异步操作,在计算机的世界里面,没有什么是再封一层做不到的,如果有,那一定就是再封两层,所以我们将packaged_task和thread再向上封装,就形成了最后的async接口,这个接口直接返回future,然后在某个时间点完成异步操作。
某个时间点取决于 async 当前的执行策略:
1. std::launch::async:建立一个线程执行指定的异步操作回传到future。
2. std::launch::deferred:将该操作的触发时间,延迟到future.get被调用的时候。
分析如下代码:
#include <iostream>#include <future>#include <thread>#include <chrono>// 模拟一个耗时任务int compute(int x) { std::cout << "Start computation with x = " << x << " in thread " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作 return x * x;}int main() { // 使用 std::async 的 std::launch::async 模式 std::future<int> async_future = std::async(std::launch::async, compute, 10); // 使用 std::async 的 std::launch::deferred 模式 std::future<int> deferred_future = std::async(std::launch::deferred, compute, 20); // 主线程继续执行其他任务 std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl; // deferred 模式任务尚未启动,只有调用 `get()` 时才会运行 std::cout << "Waiting for deferred result..." << std::endl; std::cout << "Deferred result: " << deferred_future.get() << std::endl; // 在调用 get() 时任务才运行 // 查看任务是否已启动(async 模式的任务已经开始) std::cout << "Waiting for async result..." << std::endl; std::cout << "Async result: " << async_future.get() << std::endl; // 获取结果并等待 async 模式完成 return 0;}得到结果:
Main thread ID: 135193224783680Waiting for deferred result...Deferred result: Start computation with x = 20 in thread 135193224783680Start computation with x = 10 in thread 135193218250304400Waiting for async result...Async result: 100可以观察到,在输出deferred模式的future值时会感受到2秒的明显停顿且thread id和主线程相同,而async模式的future值瞬间输出,输出的线程thread id和主线程不同,因为deferred模式下,并没有额外的线程产生,依靠主线程执行了对future的赋值操作,在async模式下,会额外创建一个新的线程异步执行操作。


被折叠的 条评论
为什么被折叠?



