什么是std :: promise?

Ker*_* SB 367 c++ multithreading standard-library promise c++11

我相当熟悉C++ 11的std::thread,std::asyncstd::future部件(例如见这个答案),这是直接的.

但是,我不能完全理解std::promise它是什么,它做什么以及在哪种情况下最好使用它.标准文档本身不包含其类概要之外的大量信息,也不仅仅是:: thread.

有人可以给出一个简短,简洁的例子,说明std::promise需要哪种情况以及最惯用的解决方案?

Ker*_* SB 477

我现在对情况了解得更好了(由于这里的答案,这个数字不小!),所以我想我加了一点自己的写作.


C++ 11中有两个截然不同但相关的概念:异步计算(在其他地方调用的函数)和并发执行(一个线程,它可以同时工作).这两者是有些正交的概念.异步计算只是一种不同的函数调用,而一个线程是一个执行上下文.线程本身很有用,但为了讨论的目的,我将它们视为实现细节.


异步计算有一个抽象层次结构.例如,假设我们有一个带有一些参数的函数:

int foo(double, char, bool);
Run Code Online (Sandbox Code Playgroud)

首先,我们有模板std::future<T>,它代表了未来的类型值T.可以通过成员函数检索该值get(),该函数通过等待结果有效地同步程序.或者,将来支持wait_for(),可用于探测结果是否已经可用.应将期货视为普通退货类型的异步直接替代品.对于我们的示例函数,我们期望a std::future<int>.

现在,从层次结构开始,从最高级别到最低级别:

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

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

    我们对细节几乎没有控制权.特别是,我们甚至不知道函数是同时执行,串行执行get()还是通过其他一些黑魔法执行.但是,在需要时很容易获得结果:

    auto res = fut.get();  // is an int
    
    Run Code Online (Sandbox Code Playgroud)
  2. 我们现在可以考虑如何以我们控制的方式实现类似的东西.例如,我们可能会坚持在单独的线程中执行该函数.我们已经知道我们可以通过类提供单独的线程.asyncstd::thread

    下一个较低的抽象级别正是如此:std::packaged_task.这是一个包装函数并为函数返回值提供未来的模板,但对象本身是可调用的,并且由用户自行决定调用它.我们可以像这样设置:

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

    一旦我们调用任务并且呼叫完成,未来就会变得准备就绪.这是单独线程的理想工作.我们必须确保任务移动到线程中:

    std::thread thr(std::move(tsk), 1.5, 'x', false);
    
    Run Code Online (Sandbox Code Playgroud)

    线程立即开始运行.我们既detach可以,也join可以在范围的最后或任何时候(例如使用Anthony Williams的scoped_thread包装器,它应该在标准库中).但是,使用的细节与std::thread我们无关; 只是一定要thr最终加入或分离.重要的是,只要函数调用完成,我们的结果就准备好了:

    auto res = fut.get();  // as before
    
    Run Code Online (Sandbox Code Playgroud)
  3. 现在我们降到最低级别:我们如何实现打包任务?这就是它的std::promise用武之地.承诺是与未来沟通的基石.主要步骤如下:

    • 调用线程做出承诺.

    • 调用线程从promise获得未来.

    • 承诺以及函数参数被移动到一个单独的线程中.

    • 新线程执行函数并填充履行承诺.

    • 原始线程检索结果.

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

    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
    };
    
    Run Code Online (Sandbox Code Playgroud)

    该模板的使用与基本相同std::packaged_task.请注意,移动整个任务包含移动承诺.在更临时的情况下,还可以将promise对象显式地移动到新线程中并使其成为线程函数的函数参数,但是像上面那样的任务包装器似乎是一种更灵活,更少侵入性的解决方案.


做例外

承诺与例外密切相关.仅承诺的接口不足以完全传达其状态,因此每当对promise的操作没有意义时抛出异常.所有异常都是类型std::future_error,源自std::logic_error.首先,描述一些约束:

  • 默认构造的promise是非活动的.不活跃的承诺可能会死亡而没有后果.

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

  • 如果未来要被消费,则必须通过承诺来满足承诺,set_value()或者set_exception()在其生命周期结束之前设置异常.满意的承诺可以在没有后果的情况下死亡,并且get()在将来可用.带有异常的promise将get()在将来调用时引发存储的异常.如果承诺既没有价值也没有例外,那么召唤get()未来会引发"破坏承诺"的例外.

这是一个小测试系列,以展示这些各种特殊行为.首先,线束:

#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;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在进行测试.

案例1:不活跃的承诺

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

案例2:积极承诺,未使用

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

案例3:太多的未来

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

案例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".
Run Code Online (Sandbox Code Playgroud)

案例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();
}
Run Code Online (Sandbox Code Playgroud)

如果有一个以上的相同抛出异常或者set_valueset_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
Run Code Online (Sandbox Code Playgroud)

案例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();
}
Run Code Online (Sandbox Code Playgroud)

  • 很好的答案,谢谢你的帮助.关于std :: async的部分,我记得我们可以确定它会产生另一个线程或与flag同步工作(std :: launch :: async,std :: launch :: deferred) (8认同)
  • 最后`got()`我的'未来'在你的惊人解释的'promise`上学习线程支持库! (3认同)

Jon*_*ely 180

用[futures.state]的话说,a std::future异步返回对象("从共享状态读取结果的对象"),a std::promise异步提供者("向共享状态提供结果的对象"),即a promise是您设置结果的东西,因此您可以从相关的未来获得它.

异步提供程序最初创建未来引用的共享状态.std::promise是一种异步提供者,std::packaged_task是另一种,内部细节std::async是另一种.其中每个都可以创建一个共享状态,并为您提供std::future共享该状态,并可以使状态准备就绪.

std::async是一个更高级别的便捷实用程序,它为您提供异步结果对象,并在内部负责创建异步提供程序并在任务完成时使共享状态就绪.您可以使用std::packaged_task(或std::bindstd::promise)和a 来模拟它,std::thread但它更安全,更容易使用std::async.

std::promise当你想要将异步结果传递给将来时,它是一个较低级别,但是使得结果准备好的代码不能被包装在适合传递的单个函数中std::async.例如,您可能有一个包含多个promises和关联futures 的数组,并且有一个线程可以执行多次计算并在每个promise上设置结果.async只允许你返回一个结果,返回你需要async多次调用的几个,这可能会浪费资源.

  • 可能浪费资源?如果该代码无法并行化,可能是不正确的. (10认同)

Pau*_*bel 32

Bartosz Milewski提供了很好的写作.

C++将future的实现拆分为一组小块

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

promise是一种工具,用于将执行函数的线程的返回值(或异常)传递给函数future中的线程.

...

未来是围绕承诺通道的接收端构建的同步对象.

因此,如果您想使用未来,最终会得到一个用于获取异步处理结果的承诺.

该页面的一个例子是:

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
Run Code Online (Sandbox Code Playgroud)

  • 看到线程构造函数中的promise最终使得一分钱掉了下来.Bartosz的文章可能不是最好的,但它确实解释了元素如何结合在一起.谢谢. (3认同)

Dav*_*eas 27

在粗略的近似中,您可以将其std::promise视为a的另一端std::future(这是错误的,但为了说明,您可以认为它就像是).通信通道的消费者端将使用a std::future来从共享状态消耗数据,而生产者线程将使用a std::promise来写入共享状态.

  • @KerrekSB:`std :: async`可以在概念上(这不是标准规定的)被理解为创建`std :: promise`的函数,将其推送到线程池(各种类型,可能是线程池,可能是一个新线程,...)并将关联的`std :: future`返回给调用者.在客户端,您将等待`std :: future`,另一端的线程将计算结果并将其存储在`std :: promise`中.注意:标准需要*shared state*和`std :: future`,但在这个特定的用例中不需要`std :: promise`. (11认同)
  • @KerrekSB:`std :: future`不会在线程上调用`join`,它有一个指向*共享状态*的指针,它是实际的通信缓冲区.*shared state*有一个同步机制(可能是`std :: function` +`std :: condition_variable`来锁定调用者,直到`std :: promise`被完成.线程的执行与所有这些正交,在许多实现中,您可能会发现`std :: async`不是由新线程执行的,而是由连接的新线程执行,而是由生命周期延伸到程序结束的线程池执行. (6认同)
  • @JonathanWakely:这并不意味着它必须在新线程中执行,只是它必须异步执行*as-if*它是在新创建的线程中运行的.`std :: async`的主要优点是运行时库可以根据要创建的线程数做出正确的决定,在大多数情况下,我会期望使用线程池的运行时.目前VS2012确实使用了引擎盖下的线程池,并且它没有违反*as-if*规则.请注意,对于此特定*as-if*,几乎不需要保证. (2认同)

kjp*_*kjp 11

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


小智 8

异步处理中确实有3个核心实体.C++ 11目前主要关注其中的2个.

异步运行某些逻辑所需的核心内容是:

  1. 任务(逻辑封装成一些仿函数对象),将运行"某处".
  2. 实际处理节点 -线程,处理等时,他们被提供给它运行这样的仿函数.查看"命令"设计模式,以便了解基本工作线程池如何执行此操作.
  3. 结果句柄:有人需要的是结果,而需要一个对象,将得到它为他们.对于OOP和其他原因,任何等待或同步都应该在此句柄的API中完成.

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

根据Herb Sutter和C++ 11脑信任中的其他人的说法,有一些尝试性的计划可以添加一个std::executor类似于Java的 - 将成为线程池的基础和逻辑上类似的设置(2).也许我们会在C++ 2014中看到它,但我的赌注更像是C++ 17(如果他们为这些标准制定标准,上帝会帮助我们).


Ric*_*ers 6

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

您可以将其视为一个线程创建提供数据的承诺,而另一个线程将来收集承诺.这种机制只能使用一次.

承诺/未来机制只是一个方向,从使用set_value()a方法std::promise的线程到使用get()a std::future接收数据的线程.如果get()未来的方法被多次调用,则会生成异常.

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

使用技术规范N4663编程语言 -协同程序的C++扩展和Visual Studio 2017 C++编译器支持的协议co_await,还可以使用std::futurestd::async编写协同程序功能.请参阅/sf/answers/3552712831/中的讨论和示例,其中有一节讨论了std::futurewith 的用法co_await.

下面的示例代码是一个简单的Visual Studio 2013 Windows控制台应用程序,它使用了一些C++ 11并发类/模板和其他功能.它说明了promise/future的使用,它可以很好地工作,自治线程可以执行某些任务和停止,以及需要更多同步行为的用途,并且由于需要多个通知,promise/future对不起作用.

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

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

第二部分main()创建两个线程和一组队列,以允许从主线程到两个创建线程中的每一个的多条消息.我们不能使用std::promisestd::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;
}
Run Code Online (Sandbox Code Playgroud)

这个简单的应用程序创建以下输出.

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
Run Code Online (Sandbox Code Playgroud)