What is std::promise?

孤者浪人 提交于 2019-11-26 18:02:23
Jonathan Wakely

In the words of [futures.state] a std::future is an asynchronous return object ("an object that reads results from a shared state") and a std::promise is an asynchronous provider ("an object that provides a result to a shared state") i.e. a promise is the thing that you set a result on, so that you can get it from the associated future.

The asynchronous provider is what initially creates the shared state that a future refers to. std::promise is one type of asynchronous provider, std::packaged_task is another, and the internal detail of std::async is another. Each of those can create a shared state and give you a std::future that shares that state, and can make the state ready.

std::async is a higher-level convenience utility that gives you an asynchronous result object and internally takes care of creating the asynchronous provider and making the shared state ready when the task completes. You could emulate it with a std::packaged_task (or std::bind and a std::promise) and a std::thread but it's safer and easier to use std::async.

std::promise is a bit lower-level, for when you want to pass an asynchronous result to the future, but the code that makes the result ready cannot be wrapped up in a single function suitable for passing to std::async. For example, you might have an array of several promises and associated futures and have a single thread which does several calculations and sets a result on each promise. async would only allow you to return a single result, to return several you would need to call async several times, which might waste resources.

I understand the situation a bit better now (in no small amount due to the answers here!), so I thought I add a little write-up of my own.


There are two distinct, though related, concepts in C++11: Asynchronous computation (a function that is called somewhere else), and concurrent execution (a thread, something that does work concurrently). The two are somewhat orthogonal concepts. Asynchronous computation is just a different flavour of func­tion call, while a thread is an execution context. Threads are useful in their own right, but for the pur­pose of this discussion, I will treat them as an implementation detail.


There is a hierarchy of abstraction for asynchronous computation. For example's sake, suppose we have a function that takes some arguments:

int foo(double, char, bool);

First off, we have the template std::future<T>, which represents a future value of type T. The val­ue can be retrieved via the member function get(), which effectively synchronizes the program by wait­ing for the result. Alternatively, a future supports wait_for(), which can be used to probe whether or not the result is already available. Futures should be thought of as the asynchronous drop-in re­place­ment for ordinary return types. For our example function, we expect a std::future<int>.

Now, on to the hierarchy, from highest to lowest level:

  1. std::async: The most convenient and straight-forward way to perform an asynchronous com­pu­ta­tion is via the async function template, which returns the matching future immediately:

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

    We have very little control over the details. In particular, we don't even know if the function is exe­cu­ted concurrently, serially upon get(), or by some other black magic. However, the result is easily ob­tained when needed:

    auto res = fut.get();  // is an int
    
  2. We can now consider how to implement something like async, but in a fashion that we control. For example, we may insist that the function be executed in a separate thread. We already know that we can provide a separate thread by means of the std::thread class.

    The next lower level of abstraction does exactly that: std::packaged_task. This is a template that wraps a function and provides a future for the functions return value, but the object itself is call­able, and calling it is at the user's discretion. We can set it up like this:

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

    The future becomes ready once we call the task and the call completes. This is the ideal job for a se­pa­rate thread. We just have to make sure to move the task into the thread:

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

    The thread starts running immediately. We can either detach it, or have join it at the end of the scope, or whenever (e.g. using Anthony Williams's scoped_thread wrapper, which really should be in the standard library). The details of using std::thread don't concern us here, though; just be sure to join or detach thr eventually. What matters is that whenever the function call finishes, our result is ready:

    auto res = fut.get();  // as before
    
  3. Now we're down to the lowest level: How would we implement the packaged task? This is where the std::promise comes in. The promise is the building block for communicating with a future. The principal steps are these:

    • The calling thread makes a promise.

    • The calling thread obtains a future from the promise.

    • The promise, along with function arguments, are moved into a separate thread.

    • The new thread executes the function and populates fulfills the promise.

    • The original thread retrieves the result.

    As an example, here's our very own "packaged task":

    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
    };
    

    Usage of this template is essentially the same as that of std::packaged_task. Note that moving the entire task subsumes moving the promise. In more ad-hoc situations, one could also move a promise object explicitly into the new thread and make it a function argument of the thread function, but a task wrapper like the one above seems like a more flexible and less intrusive solution.


Making exceptions

Promises are intimately related to exceptions. The interface of a promise alone is not enough to convey its state completely, so exceptions are thrown whenever an operation on a promise does not make sense. All exceptions are of type std::future_error, which derives from std::logic_error. First off, a description of some constraints:

  • A default-constructed promise is inactive. Inactive promises can die without consequence.

  • A promise becomes active when a future is obtained via get_future(). However, only one future may be obtained!

  • A promise must either be satisfied via set_value() or have an exception set via set_exception() before its lifetime ends if its future is to be consumed. A satisfied promise can die without consequence, and get() becomes available on the future. A promise with an exception will raise the stored exception upon call of get() on the future. If the promise dies with neither value nor exception, calling get() on the future will raise a "broken promise" exception.

Here is a little test series to demonstrate these various exceptional behaviours. First, the harness:

#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;
    }
}

Now on to the tests.

Case 1: Inactive promise

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

Case 2: Active promise, unused

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

Case 3: Too many futures

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

Case 4: Satisfied promise

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".

Case 5: Too much satisfaction

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();
}

The same exception is thrown if there is more than one of either of set_value or set_exception.

Case 6: Exception

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

Case 7: Broken promise

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 provides a good writeup.

C++ splits the implementation of futures into a set of small blocks

std::promise is one of these parts.

A promise is a vehicle for passing the return value (or an exception) from the thread executing a function to the thread that cashes in on the function future.

...

A future is the synchronization object constructed around the receiving end of the promise channel.

So, if you want to use a future, you end up with a promise that you use to get the result of the asynchronous processing.

An example from the page is:

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

In a rough approximation you can consider std::promise as the other end of a std::future (this is false, but for illustration you can think as if it was). The consumer end of the communication channel would use a std::future to consume the datum from the shared state, while the producer thread would use a std::promise to write to the shared state.

std::promise is the channel or pathway for information to be returned from the async function. std::future is the synchronization mechanism thats makes the caller wait until the return value carried in the std::promise is ready(meaning its value is set inside the function).

Zack Yezek

There are really 3 core entities in asynchronous processing. C++11 currently focuses on 2 of them.

The core things you need to run some logic asynchronously are:

  1. The task (logic packaged as some functor object) that will RUN 'somewhere'.
  2. The actual processing node - a thread, a process, etc. that RUNS such functors when they are provided to it. Look at the "Command" design pattern for a good idea of how a basic worker thread pool does this.
  3. The result handle: Somebody needs that result, and needs an object that will GET it for them. For OOP and other reasons, any waiting or synchronization should be done in this handle's APIs.

C++11 calls the things I speak of in (1) std::promise, and those in (3) std::future. std::thread is the only thing provided publicly for (2). This is unfortunate because real programs need to manage thread & memory resources, and most will want tasks to run on thread pools instead of creating & destroying a thread for every little task (which almost always causes unnecessary performance hits by itself and can easily create resource starvation that is even worse).

According to Herb Sutter and others in the C++11 brain trust, there are tentative plans to add a std::executor that- much like in Java- will be the basis for thread pools and logically similar setups for (2). Maybe we'll see it in C++2014, but my bet is more like C++17 (and God help us if they botch the standard for these).

A std::promise is created as an end point for a promise/future pair and the std::future (created from the std::promise using the get_future() method) is the other end point. This is a simple, one shot method of providing a way for two threads to synchronize as one thread provides data to another thread through a message.

You can think of it as one thread creates a promise to provide data and the other thread collects the promise in the future. This mechanism can only be used once.

The promise/future mechanism is only one direction, from the thread which uses the set_value() method of a std::promise to the thread which uses the get() of a std::future to receive the data. An exception is generated if the get() method of a future is called more than once.

If the thread with the std::promise has not used set_value() to fulfill its promise then when the second thread calls get() of the std::future to collect the promise, the second thread will go into a wait state until the promise is fulfilled by the first thread with the std::promise when it uses the set_value() method to send the data.

With the proposed coroutines of Technical Specification N4663 Programming Languages — C++ Extensions for Coroutines and the Visual Studio 2017 C++ compiler support of co_await, it is also possible to use std::future and std::async to write coroutine functionality. See the discussion and example in https://stackoverflow.com/a/50753040/1466970 which has as one section that discusses the use of std::future with co_await.

The following example code, a simple Visual Studio 2013 Windows console application, shows using a few of the C++11 concurrency classes/templates and other functionality. It illustrates a use for promise/future which works well, autonomous threads which will do some task and stop, and a use where more synchronous behavior is required and due to the need for multiple notifications the promise/future pair does not work.

One note about this example is the delays added in various places. These delays were added only to make sure that the various messages printed to the console using std::cout would be clear and that text from the several threads would not be intermingled.

The first part of the main() is creating three additional threads and using std::promise and std::future to send data between the threads. An interesting point is where the main thread starts up a thread, T2, which will wait for data from the main thread, do something, and then send data to the third thread, T3, which will then do something and send data back to the main thread.

The second part of the main() creates two threads and a set of queues to allow multiple messages from the main thread to each of the two created threads. We can not use std::promise and std::future for this because the promise/future duo are one shot and can not be use repeatedly.

The source for the class Sync_queue is from Stroustrup's 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;
}

This simple application creates the following output.

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

The promise is the other end of the wire.

Imagine you need to retrieve the value of a future being computed by an async. However, you don't want it to be computed in the same thread, and you don't even spawn a thread "now" - maybe your software was designed to pick a thread from a pool, so you don't know who will perform che computation in the end.

Now, what do you pass to this (yet unknown) thread/class/entity? You don't pass the future, since this is the result. You want to pass something that is connected to the future and that represents the other end of the wire, so you will just query the future with no knowledge about who will actually compute/write something.

This is the promise. It is a handle connected to your future. If the future is a speaker, and with get() you start listening until some sound comes out, the promise is a microphone; but not just any microphone, it is the microphone connected with a single wire to the speaker you hold. You might know who's at the other end but you don't need to know it - you just give it and wait until the other party says something.

http://www.cplusplus.com/reference/future/promise/

One sentence explanation: furture::get() waits promse::set_value() forever.

void print_int(std::future<int>& fut) {
    int x = fut.get(); // future would wait prom.set_value forever
    std::cout << "value: " << x << '\n';
}

int main()
{
    std::promise<int> prom;                      // create promise

    std::future<int> fut = prom.get_future();    // engagement with future

    std::thread th1(print_int, std::ref(fut));  // send future to new thread

    prom.set_value(10);                         // fulfill promise
                                                 // (synchronizes with getting the future)
    th1.join();
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!