C++ | 异步接口 promise, future, packaged_task, async.

今天我们来介绍一种异步与并发编程的解决方案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): 42

packaged_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模式下,会额外创建一个新的线程异步执行操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值