424

我对 C++11 和组件(例如,请参阅此答案)相当熟悉std::thread,它们std::async直截了当的。std::future

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

有人可以给出一个简短的例子来说明std::promise需要 an 以及它是最惯用的解决方案的情况吗?

4

9 回答 9

556

我现在更好地理解了这种情况(由于这里的答案,在很大程度上!),所以我想我自己添加一点文章。


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


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

int foo(double, char, bool);

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

现在,进入层次结构,从最高到最低级别:

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

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

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

    auto res = fut.get();  // is an int
    
  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>
    

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

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

    线程立即开始运行。我们可以使用detach它,或者join在作用域的末尾使用它,或者在任何时候使用它(例如,使用 Anthony Williams 的scoped_thread包装器,它确实应该在标准库中)。不过,使用的细节与std::thread我们无关;只要确保最终加入或分离thr。重要的是,只要函数调用完成,我们的结果就准备好了:

    auto res = fut.get();  // as before
    
  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
    };
    

    该模板的用法与std::packaged_task. 请注意,移动整个任务包含移动承诺。在更特殊的情况下,也可以将 promise 对象显式移动到新线程中,并使其成为线程函数的函数参数,但是像上面这样的任务包装器似乎是一种更灵活且侵入性更小的解决方案。


例外处理

Promise 与异常密切相关。仅 Promise 的接口不足以完整地传达其状态,因此只要对 Promise 的操作没有意义,就会引发异常。所有异常都是类型std::future_error,它派生自std::logic_error. 首先,描述一些约束:

  • 默认构造的 Promise 是无效的。不活跃的承诺可能会毫无后果地消失。

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

  • 如果要使用它的未来,则必须在其生命周期结束之前通过通过set_value()或设置异常来满足承诺。set_exception()一个满意的承诺可以毫无后果地消亡,并get()在未来变得可用。带有异常的承诺将get()在未来调用时引发存储的异常。如果 Promise 既没有价值也没有异常而死,调用get()未来将引发“破碎的 Promise”异常。

这是一个小测试系列来演示这些不同的异常行为。一、线束:

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

现在开始测试。

案例 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

案例 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();
}
于 2012-09-08T23:10:05.543 回答
207

用 [futures.state] 的话来说,astd::future异步返回对象(“从共享状态读取结果的对象”),astd::promise异步提供者(“向共享状态提供结果的对象”),即 a promise 是你设置结果的东西,这样你就可以从相关的未来中得到它。

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

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

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

于 2012-06-13T17:59:00.707 回答
37

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
于 2012-06-12T20:34:59.097 回答
32

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

于 2012-06-12T20:30:27.180 回答
13

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

于 2012-06-12T20:42:10.683 回答
8

异步处理中确实有 3 个核心实体。C++11 目前专注于其中的 2 个。

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

  1. 将在“某处”运行的任务(逻辑打包为一些仿函数对象)。
  2. 实际的处理节点——一个线程、一个进程等,当它们被提供给它时​​运行这些仿函数。查看“命令”设计模式,了解基本工作线程池是如何做到这一点的。
  3. 结果句柄:有人需要该结果,并且需要一个可以为他们获取结果的对象。出于 OOP 和其他原因,任何等待或同步都应在此句柄的 API 中完成。

std::promiseC++11 将我在 (1)和 (3) 中所说的东西称为std::futurestd::thread是为 (2) 公开提供的唯一内容。这是不幸的,因为真正的程序需要管理线程和内存资源,并且大多数人希望任务在线程池上运行,而不是为每个小任务创建和销毁线程(这几乎总是会导致不必要的性能损失,并且可以轻松创建资源更糟糕的饥饿)。

根据 Herb Sutter 和 C++11 智囊团的其他人的说法,有初步计划添加一个std::executor- 就像在 Java 中一样 - 将作为线程池的基础,并且在逻辑上类似于 (2) 的设置。也许我们会在 C++2014 中看到它,但我的赌注更像是 C++17(如果他们搞砸了这些标准,上帝会帮助我们)。

于 2014-01-23T23:33:14.913 回答
7

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

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

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

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

借助Technical Specification N4663 Programming Languages — C++ Extensions for Coroutines的提议协程和 Visual Studio 2017 C++ 编译器支持co_await,还可以使用std::futurestd::async编写协程功能。请参阅https://stackoverflow.com/a/50753040/1466970中的讨论和示例,其中有一节讨论了std::futurewith的使用co_await

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

关于此示例的一个注意事项是在各个地方添加的延迟。添加这些延迟只是为了确保打印到控制台的各种消息std::cout清晰,并且来自多个线程的文本不会混合。

第一部分main()是创建三个额外的线程并在线程之间使用std::promisestd::future发送数据。一个有趣的地方是主线程启动了一个线程,T2,它将等待来自主线程的数据,做一些事情,然后将数据发送到第三个线程,T3,然后它会做一些事情并将数据发送回主线程。

第二部分main()创建两个线程和一组队列,以允许来自主线程的多条消息发送到两个创建的线程中的每一个。我们不能使用std::promiseand std::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;
}

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

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
于 2016-01-21T02:51:40.617 回答
1

承诺是电线的另一端。

想象一下,您需要检索由 afuture计算的 a 的值async。但是,您不希望它在同一个线程中计算,甚至“现在”都不会产生线程 - 也许您的软件设计为从池中选择一个线程,所以您不知道谁会最后执行 che 计算。

现在,你将什么传递给这个(但未知的)线程/类/实体?你没有通过future,因为这是结果。您想传递连接future并且代表电线另一端的东西,因此您将只查询future不知道谁将实际计算/编写某些东西。

这是promise. 它是一个连接到您的future. 如果future扬声器,并且随着get()您开始聆听直到有声音出现,则promise麦克风;但不仅仅是任何麦克风,它是通过单根电线连接到您持有的扬声器的麦克风。你可能知道谁在另一端,但你不需要知道——你只要给它,等对方说些什么。

于 2017-06-29T21:24:24.627 回答
0

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

一句话解释:furture::get()永远等待promse::set_value()。

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;
}
于 2019-06-06T09:15:57.327 回答