什么是std :: promise?

我很熟悉新的标准库的std::threadstd::asyncstd::future组件(例如见这个答案 ),这很简单。

然而,我不能完全掌握std::promise是什么,它在什么情况下最好使用。 标准文档本身并不包含除了类纲要之外的大量信息,也不仅仅是:: thread 。

有人可以给一个std::promise的情况下简短,简洁的例子,它是最习惯的解决scheme?

用[futures.state]的话来说, std::future是一个asynchronous返回对象 (“一个读取共享状态结果的对象”), std::promise是一个asynchronous提供者 (“提供结果的对象到一个共享的状态“),即承诺是你设定的结果,以便你可以从相关的未来。

asynchronous提供者是最初创build未来引用的共享状态的东西。 std::promise是一种asynchronous提供程序, std::packaged_task是另一种types, std::async的内部细节是另一种。 每个人都可以创build一个共享状态,并为您提供一个共享该状态的std::future ,并且可以使状态准备就绪。

std::async是一个更高级的便利实用程序,它为您提供了一个asynchronous结果对象,并在内部负责创buildasynchronous提供程序,并在任务完成时使共享状态准备就绪。 你可以使用std::packaged_task (或者std::bindstd::promise )和std::thread来模拟它,但是使用std::async更安全。

std::promise有点低级,因为当你想将asynchronous结果传递给将来,但是使结果准备就绪的代码不能包含在适合传递给std::async的单个函数中。 例如,你可能有一个数组的promise和相关的future并有一个单独的线程,做了几个计算,并在每个承诺上设置一个结果。 async只会允许你返回一个单一的结果,返回几个你需要多次调用async ,这可能会浪费资源。

我现在明白情况会好一点(因为这里的答案不less),所以我想我自己写一点点。


在C ++ 11中有两个截然不同的相关概念:asynchronous计算(一个被称为别处的函数)和并发执行(一个线程 ,可以同时工作)。 这两个是有些正交的概念。 asynchronous计算只是函数调用的不同风格,而线程则是执行上下文。 线程本身是有用的,但为了讨论的目的,我将把它们作为一个实现细节。

asynchronous计算有一个抽象层次。 举例来说,假设我们有一个函数需要一些参数:

 int foo(double, char, bool); 

首先,我们有模板std::future<T> ,它表示typesT的未来值。 该值可以通过成员函数get() ,通过等待结果有效地同步程序。 或者,未来支持wait_for() ,可以用来探测结果是否已经可用。 期货应该被认为是普通回报types的asynchronous投入替代品。 对于我们的示例函数,我们期望std::future<int>

现在,从最高层到最低层的层次结构:

  1. std::async :执行asynchronous计算的最方便和直接的方法是通过async函数模板,它立即返回匹配的未来:

     auto fut = std::async(foo, 1.5, 'x', false); // is a std::future<int> 

    我们很less控制细节。 特别是,我们甚至不知道这个函数是同时执行的,在get()还是其他一些黑魔法中被连续执行。 但是,如果需要,结果很容易获得:

     auto res = fut.get(); // is an int 
  2. 现在我们可以考虑如何实现async一样的东西,但以我们控制的方式。 例如,我们可能会坚持要在一个单独的线程中执行该函数。 我们已经知道我们可以通过std::thread类来提供一个单独的线程。

    下一个较低的抽象层次就是: std::packaged_task 。 这是一个封装函数的模板,为函数的返回值提供了一个未来,但是对象本身是可调用的,调用它是由用户决定的。 我们可以这样设置:

     std::packaged_task<int(double, char, bool)> tsk(foo); auto fut = tsk.get_future(); // is a std::future<int> 

    一旦我们呼叫任务并且呼叫完成,未来就准备好了。 这是一个单独的线程的理想工作。 我们只需要确保任务移动到线程中:

     std::thread thr(std::move(tsk), 1.5, 'x', false); 

    线程立即开始运行。 我们可以detach它,或者在范围的末尾join它,或者在任何时候(例如使用Anthony Williams的scoped_thread包装器,这实际上应该在标准库中)。 虽然使用std::thread的细节在这里并不关心我们; 只要确定最终join或脱离thr 。 重要的是,只要函数调用完成,我们的结果就准备好了:

     auto res = fut.get(); // as before 
  3. 现在我们已经到了最低层:我们将如何实现打包的任务? 这就是std::promise地方std::promise是与未来交stream的基石。 主要步骤是:

    • 调用线程作出承诺。

    • 调用线程从诺言中获得未来。

    • 这个承诺和函数参数一起被移到一个单独的线程中。

    • 新线程执行该function并填充满足承诺。

    • 原始线程检索结果。

    举个例子,这是我们自己的“打包任务”:

     template <typename> class my_task; template <typename R, typename ...Args> class my_task<R(Args...)> { std::function<R(Args...)> fn; std::promise<R> pr; // the promise of the result public: template <typename ...Ts> explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { } template <typename ...Ts> void operator()(Ts &&... ts) { pr.set_value(fn(std::forward<Ts>(ts)...)); // fulfill the promise } std::future<R> get_future() { return pr.get_future(); } // disable copy, default move }; 

    这个模板的用法和std::packaged_task基本相同。 请注意,移动整个任务包括移动诺言。 在更特殊的情况下,还可以将一个promise对象明确地移动到新的线程中,并将其作为线程函数的一个函数参数,但是像上面这样的任务包装似乎是一个更灵活和更less侵入的解决scheme。


例外

承诺与例外密切相关。 单单一个承诺的接口不足以完全传达它的状态,所以只要承诺的操作没有意义,就会抛出exception。 所有exception的types都是std::future_error ,它来源于std::logic_error 。 首先,描述一些限制条件:

  • 默认构build的promise是不活动的。 不作为的承诺会死而无憾。

  • 当未来通过get_future()获得时,承诺变得活跃。 但是,只有一个未来可能会被获得!

  • 一个承诺必须通过set_value()来满足,或者在它的生命周期结束之前通过set_exception()设置一个exception,如果它的未来将被消耗的话。 一个满意的承诺可以死而无后事, get()在未来可用。 一个有例外的承诺会在将来调用get()时引发存储的exception。 如果诺言既没有价值也没有例外,在将来调用get()将会引发一个“破坏的承诺”exception。

这里有一些testing系列来演示这些各种特殊的行为。 首先,

 #include <iostream> #include <future> #include <exception> #include <stdexcept> int test(); int main() { try { return test(); } catch (std::future_error const & e) { std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl; } catch (std::exception const & e) { std::cout << "Standard exception: " << e.what() << std::endl; } catch (...) { std::cout << "Unknown exception." << std::endl; } } 

现在进行testing。

案例1:无效的承诺

 int test() { std::promise<int> pr; return 0; } // fine, no problems 

案例2:主动承诺,未使用

 int test() { std::promise<int> pr; auto fut = pr.get_future(); return 0; } // fine, no problems; fut.get() would block indefinitely 

案例3:期货太多

 int test() { std::promise<int> pr; auto fut1 = pr.get_future(); auto fut2 = pr.get_future(); // Error: "Future already retrieved" return 0; } 

案例4:满意的承诺

 int test() { std::promise<int> pr; auto fut = pr.get_future(); { std::promise<int> pr2(std::move(pr)); pr2.set_value(10); } return fut.get(); } // Fine, returns "10". 

案例5:太满意了

 int test() { std::promise<int> pr; auto fut = pr.get_future(); { std::promise<int> pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // Error: "Promise already satisfied" } return fut.get(); } 

如果set_valueset_exception有一个以上,则抛出同样的exception。

案例6:例外

 int test() { std::promise<int> pr; auto fut = pr.get_future(); { std::promise<int> pr2(std::move(pr)); pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo"))); } return fut.get(); } // throws the runtime_error exception 

案例7:违约承诺

 int test() { std::promise<int> pr; auto fut = pr.get_future(); { std::promise<int> pr2(std::move(pr)); } // Error: "broken promise" return fut.get(); } 

Bartosz Milewski提供了一个很好的写作。

C ++将期货的实现分解成一组小块

std :: promise是这些部分之一。

承诺是将执行函数的线程返回值(或exception)传递给未来函数的线程的工具。

未来是在承诺通道的接收端周围构build的同步对象。

所以,如果你想使用未来,你最终会得到一个承诺,用来获得asynchronous处理的结果。

来自该页面的示例是:

 promise<int> intPromise; future<int> intFuture = intPromise.get_future(); std::thread t(asyncFun, std::move(intPromise)); // do some other stuff int result = intFuture.get(); // may throw MyException 

粗略地说,你可以把std::promise当作std::future的另一端(这是错误的 ,但是为了说明你可以认为它是如此)。 通信通道的消费者端将使用std::future来从共享状态消耗数据,而生产者线程将使用std::promise来写入共享状态。

std::promise是从asynchronous函数返回信息的通道或path。 std::future是同步机制,它使得调用者等待,直到std::promise携带的返回值就绪(意味着它的值在函数内部被设置)。

asynchronous处理中确实有3个核心实体。 目前C ++ 11着重于其中的两个。

你需要asynchronous运行一些逻辑的核心是:

  1. 任务 (逻辑打包为一些函子对象)将运行“某处”。
  2. 实际的处理节点 – 一个线程,一个进程等等,当它们被提供给它时​​,运行这样的函子。 查看“Command”devise模式,了解基本工作程序线程池如何执行此操作。
  3. 结果句柄 :有人需要那个结果,并需要一个对象来获取它。 对于OOP和其他原因,任何等待或同步应该在这个句柄的API中完成。

C ++ 11调用了我在(1) std::promise和(3) std::future提到的东西。 (2)公开提供的std::thread是唯一的东西。 这是不幸的,因为真正的程序需要pipe理线程和内存资源,并且大多数人会希望任务在线程池上运行,而不是为每个小任务创build和销毁线程(这几乎总是会导致不必要的性能命中,并且可以轻松地创build资源饥饿更糟糕)。

根据Herb Sutter和C ++ 11脑信任中的其他人的介绍,添加一个std::executor尝试性计划(很像Java中的)将成为(2)的线程池和逻辑上类似的设置的基础。 也许我们会在C ++ 2014中看到它,但我敢打赌更像C ++ 17(如果他们把这些标准弄糊涂,上帝会帮助我们)。

一个std::promise被创build为一个promise / future对的终点,而std::future (使用get_future()方法从std :: promise创build)是另一个终点。 这是一种简单的一次性方法,它为两个线程同步提供了一种方法,因为一个线程通过消息向另一个线程提供数据。

你可以把它看作是一个线程创造了提供数据的承诺,另一个线程收集了未来的承诺。 这个机制只能使用一次。

promise / future机制只是一个方向,从使用std::promiseset_value()方法的线程到使用std::futureget()接收数据的线程。 如果未来的get()方法被多次调用,则会生成一个exception。

如果具有std::promise的线程没有使用set_value()来履行承诺,那么当第二个线程调用std::future get()来收集承诺时,第二个线程将进入等待状态,直到当它使用set_value()方法发送数据时,第一个线程使用std::promise promise来实现std::promise

以下示例代码(简单的Visual Studio 2013 Windows控制台应用程序)显示了使用一些C ++ 11并发类/模板和其他function。 它说明了承诺/未来的使用,它能够很好地工作,自治的线程将完成一些任务并停止,以及需要更多同步行为的使用,并且由于需要多个通知,承诺/未来对不起作用。

关于这个例子的一个注记是在各个地方添加的延迟。 这些延迟的添加只是为了确保使用std::cout打印到控制台的各种消息将被清除,并且来自多个线程的文本不会混杂在一起。

main()的第一部分是创build三个附加线程,并使用std::promisestd::future在线程之间发送数据。 有趣的一点是,主线程启动一个线程T2,它将等待来自主线程的数据,执行某些操作,然后将数据发送到第三个线程T3,T3将执行某些操作并将数据发送回主线程。

main()的第二部分创build两个线程和一组队列,以允许从主线程到两个创build的线程中的每一个的多个消息。 我们不能使用std::promisestd::future ,因为promise / future二人组是一个镜头,不能重复使用。

Sync_queue的来源来自Stroustrup的The C ++ Programming Language:4th Edition。

 // cpp_threads.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> #include <thread> // std::thread is defined here #include <future> // std::future and std::promise defined here #include <list> // std::list which we use to build a message queue on. static std::atomic<int> kount(1); // this variable is used to provide an identifier for each thread started. //------------------------------------------------ // create a simple queue to let us send notifications to some of our threads. // a future and promise are one shot type of notifications. // we use Sync_queue<> to have a queue between a producer thread and a consumer thread. // this code taken from chapter 42 section 42.3.4 // The C++ Programming Language, 4th Edition by Bjarne Stroustrup // copyright 2014 by Pearson Education, Inc. template<typename Ttype> class Sync_queue { public: void put(const Ttype &val); void get(Ttype &val); private: std::mutex mtx; // mutex used to synchronize queue access std::condition_variable cond; // used for notifications when things are added to queue std::list <Ttype> q; // list that is used as a message queue }; template<typename Ttype> void Sync_queue<Ttype>::put(const Ttype &val) { std::lock_guard <std::mutex> lck(mtx); q.push_back(val); cond.notify_one(); } template<typename Ttype> void Sync_queue<Ttype>::get(Ttype &val) { std::unique_lock<std::mutex> lck(mtx); cond.wait(lck, [this]{return !q.empty(); }); val = q.front(); q.pop_front(); } //------------------------------------------------ // thread function that starts up and gets its identifier and then // waits for a promise to be filled by some other thread. void func(std::promise<int> &jj) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future<int> intFuture(jj.get_future()); auto ll = intFuture.get(); // wait for the promise attached to the future std::cout << " func " << myId << " future " << ll << std::endl; } // function takes a promise from one thread and creates a value to provide as a promise to another thread. void func2(std::promise<int> &jj, std::promise<int>&pp) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future<int> intFuture(jj.get_future()); auto ll = intFuture.get(); // wait for the promise attached to the future auto promiseValue = ll * 100; // create the value to provide as promised to the next thread in the chain pp.set_value(promiseValue); std::cout << " func2 " << myId << " promised " << promiseValue << " ll was " << ll << std::endl; } // thread function that starts up and waits for a series of notifications for work to do. void func3(Sync_queue<int> &q, int iBegin, int iEnd, int *pInts) { int myId = std::atomic_fetch_add(&kount, 1); int ll; q.get(ll); // wait on a notification and when we get it, processes it. while (ll > 0) { std::cout << " func3 " << myId << " start loop base " << ll << " " << iBegin << " to " << iEnd << std::endl; for (int i = iBegin; i < iEnd; i++) { pInts[i] = ll + i; } q.get(ll); // we finished this job so now wait for the next one. } } int _tmain(int argc, _TCHAR* argv[]) { std::chrono::milliseconds myDur(1000); // create our various promise and future objects which we are going to use to synchronise our threads // create our three threads which are going to do some simple things. std::cout << "MAIN #1 - create our threads." << std::endl; // thread T1 is going to wait on a promised int std::promise<int> intPromiseT1; std::thread t1(func, std::ref(intPromiseT1)); // thread T2 is going to wait on a promised int and then provide a promised int to thread T3 std::promise<int> intPromiseT2; std::promise<int> intPromiseT3; std::thread t2(func2, std::ref(intPromiseT2), std::ref(intPromiseT3)); // thread T3 is going to wait on a promised int and then provide a promised int to thread Main std::promise<int> intPromiseMain; std::thread t3(func2, std::ref(intPromiseT3), std::ref(intPromiseMain)); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2 - provide the value for promise #1" << std::endl; intPromiseT1.set_value(22); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2.2 - provide the value for promise #2" << std::endl; std::this_thread::sleep_for(myDur); intPromiseT2.set_value(1001); std::this_thread::sleep_for(myDur); std::cout << "MAIN #2.4 - set_value 1001 completed." << std::endl; std::future<int> intFutureMain(intPromiseMain.get_future()); auto t3Promised = intFutureMain.get(); std::cout << "MAIN #2.3 - intFutureMain.get() from T3. " << t3Promised << std::endl; t1.join(); t2.join(); t3.join(); int iArray[100]; Sync_queue<int> q1; // notification queue for messages to thread t11 Sync_queue<int> q2; // notification queue for messages to thread t12 std::thread t11(func3, std::ref(q1), 0, 5, iArray); // start thread t11 with its queue and section of the array std::this_thread::sleep_for(myDur); std::thread t12(func3, std::ref(q2), 10, 15, iArray); // start thread t12 with its queue and section of the array std::this_thread::sleep_for(myDur); // send a series of jobs to our threads by sending notification to each thread's queue. for (int i = 0; i < 5; i++) { std::cout << "MAIN #11 Loop to do array " << i << std::endl; std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete q1.put(i + 100); std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete q2.put(i + 1000); std::this_thread::sleep_for(myDur); // sleep a moment for I/O to complete } // close down the job threads so that we can quit. q1.put(-1); // indicate we are done with agreed upon out of range data value q2.put(-1); // indicate we are done with agreed upon out of range data value t11.join(); t12.join(); return 0; } 

这个简单的应用程序创build以下输出。

 MAIN #1 - create our threads. MAIN #2 - provide the value for promise #1 func 1 future 22 MAIN #2.2 - provide the value for promise #2 func2 2 promised 100100 ll was 1001 func2 3 promised 10010000 ll was 100100 MAIN #2.4 - set_value 1001 completed. MAIN #2.3 - intFutureMain.get() from T3. 10010000 MAIN #11 Loop to do array 0 func3 4 start loop base 100 0 to 5 func3 5 start loop base 1000 10 to 15 MAIN #11 Loop to do array 1 func3 4 start loop base 101 0 to 5 func3 5 start loop base 1001 10 to 15 MAIN #11 Loop to do array 2 func3 4 start loop base 102 0 to 5 func3 5 start loop base 1002 10 to 15 MAIN #11 Loop to do array 3 func3 4 start loop base 103 0 to 5 func3 5 start loop base 1003 10 to 15 MAIN #11 Loop to do array 4 func3 4 start loop base 104 0 to 5 func3 5 start loop base 1004 10 to 15 

承诺是电线的另一端。

想象一下,你需要检索由async计算的future的价值。 然而,你不希望它是在同一个线程中计算的,而且你甚至不会“现在”产生一个线程 – 也许你的软件被devise为从一个池中select一个线程,所以你不知道谁会最后进行计算。

现在,你传递给这个(但未知的)线程/类/实体的是什么? 你不会传递future ,因为这是结果 。 你想传递一些 future 连接的东西,代表线程的另一端 ,所以你只需要查询future ,而不知道谁会真正计算/写入什么。

这是promise 。 这是一个连接到你的future句柄 。 如果future演讲者 ,并且get()开始聆听,直到有些声音出现,那么promise就是麦克风 。 但不只是任何麦克风,它是麦克风与您持有的扬声器单线连接。 你可能知道谁在另一端,但你不需要知道它 – 你只要给它,等到对方说了什么。