Что такое std:: promise?

Я довольно хорошо знаком с C++11-х std::thread, std::async и std::future компоненты (например, см. ответ), которые являются прямыми.

однако я не совсем понимаю, что std::promise - Это, что он делает и в каких ситуациях его лучше использовать. Сам стандартный документ не содержит много информации за пределами его класса synopsis, и ни just:: thread.

не мог бы кто-нибудь дать краткий, краткий пример ситуация, когда std::promise необходимо и где это самое идиоматическое решение?

8 ответов


по словам [futures.state] a std::future Это асинхронный объект возврата ("объект, считывающий результаты из общего состояния") и std::promise это асинхронный поставщик ("объект, который предоставляет результат общему состоянию"), т. е. обещание-это то, что вы set результат, так что вы можете get это из связанного будущем.

асинхронный поставщик-это то, что изначально создает общий состояние, к которому относится будущее. std::promise является одним из типов асинхронного поставщика,std::packaged_task это еще одна, и внутренняя деталь std::async это другое. Каждый из них может создать общее состояние и даст вам std::future Это разделяет это состояние и может сделать состояние готовым.

std::async - это утилита удобства более высокого уровня, которая дает вам асинхронный объект результата и внутренне заботится о создании асинхронного поставщика и делает общее состояние готовым, когда задача завершает. Вы могли бы подражать ему с std::packaged_task (или std::bind и std::promise) и std::thread но это безопаснее и проще в использовании std::async.

std::promise немного ниже уровня, когда вы хотите передать асинхронный результат в будущее, но код, который делает результат готовым, не может быть завернут в одну функцию, подходящую для передачи в std::async. Например, у вас может быть массив из нескольких promises и связанные futures и имеют один поток, который делает несколько расчеты и устанавливает результат по каждому обещанию. async позволит вам вернуть только один результат, для возврата нескольких вам нужно будет позвонить async несколько раз, что может привести к потере ресурсов.


теперь я понимаю ситуацию немного лучше (в немалой степени из-за ответов здесь!), поэтому я подумал, что добавлю немного своей собственной записи.


в C++11 есть два различных, хотя и связанных понятия: асинхронное вычисление (функция, которая называется где-то еще) и параллельное выполнение (a нить, что-то, что работает одновременно). Эти два понятия несколько ортогональны. Асинхронные вычисления-это просто другой вкус вызов функции, в то время как поток является контекстом выполнения. Темы полезны сами по себе, но для целей этого обсуждения я буду рассматривать их как деталь реализации.


существует иерархия абстракций для асинхронных вычислений. Например, предположим, что у нас есть функция, которая принимает некоторые аргументы:

int foo(double, char, bool);

во-первых, у нас есть шаблон std::future<T>, который представляет будущее значение тип T. Значение можно получить с помощью функции-члена get(), который эффективно синхронизирует программу, ожидая результата. Кроме того, будущее поддерживает 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. теперь мы можем рассмотреть, как реализовать что-то вроде 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 это в конце области или когда угодно (например, с помощью Энтони Уильямса scoped_thread фантик, который действительно должен быть в стандартной библиотеке). Подробности использования std::thread нас здесь не касается; просто обязательно присоединяйтесь или отсоединяйтесь thr в конце концов. Важно то, что всякий раз, когда вызов функции заканчивается, наш результат готов:

    auto res = fut.get();  // as before
    
  3. теперь мы спустились на самый низкий уровень: как бы мы реализовать упакованная задача? Вот где std::promise приходит. Обещание-это строительный блок для общения с будущим. Основные шаги таковы:--44-->

    • вызывающий поток дает обещание.

    • вызывающий поток получает будущее от обещания.

    • обещание вместе с аргументами функции перемещаются в отдельный поток.

    • новый поток выполняет функцию и заполняет выполняет обещание.

    • исходный поток извлекает результат.

    в качестве примера, вот наша собственная "упакованная задача":

    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 в Новый Поток и сделать это аргумент функции функции потока, но оболочка задачи, подобная приведенной выше, кажется более гибким и менее навязчивым решением.


делая исключения

обещания тесно связаны с исключениями. Интерфейса одного обещания недостаточно, чтобы полностью передать его состояние, поэтому исключения возникают всякий раз, когда операция над обещанием не имеет смысла. Все исключения имеют тип std::future_error, который является производным от std::logic_error. Во-первых, описание некоторых ограничений:

  • построенное по умолчанию обещание неактивно. Бездействующие обещания могут умереть без последствий.

  • обещание становится активным, когда будущее получено через get_future(). Однако, только один будущее может быть получен!

  • обещание должно быть выполнено через set_value() или имеют исключение, установленное через set_exception() прежде чем его жизнь закончится, если его будущее должно быть уничтожено. Удовлетворенное обещание может умереть без последствий, и get() становится доступным в будущем. Обещание с исключением вызовет сохраненное исключение по вызову 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;
    }
}

теперь к тестам.

Случай 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();
}

то же исключение возникает, если существует более чем один из или of set_value или set_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();
}

Бартош Милевски обеспечивает хорошую рецензию.

C++ разбивает реализацию фьючерсов на множество из маленьких блоков

std:: promise-одна из этих частей.

обещание-это средство передачи возвращаемого значения (или исключение) из потока, выполняющего функцию для потока это обналичивает будущее функции.

...

будущее объект синхронизации, построенный вокруг приемный конец канала обещания.

Итак, если вы хотите использовать будущее, вы получите обещание, которое вы используете для получения результата асинхронной обработки.

пример со страницы:

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 (это false, но для иллюстрации вы можете думать, как если бы это было). Потребительский конец канала связи будет использовать std::future для использования datum из общего состояния, в то время как поток производителя будет использовать std::promise для записи в общее состояние.


std::promise - Это канал или путь для информации, возвращаемой из функции async. std::future это механизм синхронизации, который заставляет вызывающего абонента ждать, пока возвращаемое значение не будет перенесено в std::promise готово(то есть его значение задано внутри функции).


в асинхронной обработке действительно есть 3 основных объекта. В C++11 в настоящее время фокусируется на 2 из них.

основные вещи, которые вам нужно запустить некоторую логику асинхронно:

  1. на задание (логика упакована как некоторый объект функтора), который будет работать "где-то".
  2. на фактический узел обработки - поток, процесс и т. д. это запускает такие функторы, когда они предоставляются ему. Посмотрите на Шаблон проектирования "Command" для хорошего представления о том, как это делает основной пул рабочих потоков.
  3. на результат handle: кому-то нужен этот результат, и нужен объект, который получит его для них. По ООП и другим причинам любое ожидание или синхронизация должны выполняться в API этого дескриптора.

C++11 называет вещи, о которых я говорю в (1) std::promise, а те, что в (3)std::future. std::thread - это единственное, что публично предусмотрено для (2). Это прискорбно, потому что реальные программы должны управлять потоками и ресурсами памяти, и большинство из них хотят, чтобы задачи запускались в пулах потоков, а не создавали и уничтожали поток для каждой маленькой задачи (что почти всегда вызывает ненужные хиты производительности сам по себе и может легко создать голодание ресурсов, что еще хуже).

согласно Хербу Саттеру и другим в мозговом тресте C++11, есть предварительные планы добавить std::executor это-как и в Java-будет основой для пулов потоков и логически аналогичные настройки для (2). Возможно, мы увидим это в C++2014, но моя ставка больше похожа на C++17 (и Бог поможет нам, если они испортят стандарт для них).


A std::promise создается как конечная точка для пары обещание / будущее и std::future (создано из std:: promise с помощью get_future() метод) является другой конечной точкой. Это простой, один выстрел метод предоставления способ для двух потоков для синхронизации, как один поток предоставляет данные другому потоку через сообщение.

вы можете думать об этом, как один поток создает обещание предоставить данные, а другой поток собирает обещание в будущем. Этот механизм можно использовать только однажды.

механизм обещания / будущего-это только одно направление, из потока, который использует set_value() метод a std::promise к потоку, который использует get() of a std::future для получения данных. Исключение создается, если get() метод будущего вызывается не один раз.

если нить с std::promise б set_value() чтобы выполнить свое обещание, когда второй поток вызывает get() на std::future для того чтобы собрать обещание, второй поток войдет в состояние ожидания, пока обещание не будет выполнено первым потоком с std::promise при использовании set_value() метод для отправки данных.

в следующем примере кода, простого консольного приложения Visual Studio 2013 Windows, показано использование нескольких классов/шаблонов параллелизма c++11 и других функциональных возможностей. Он иллюстрирует использование для обещания / будущего, которое хорошо работает, автономных потоков, которые будут выполнять некоторые задачи и останавливаться, и использование, где более синхронное поведение требуется и из-за необходимости нескольких уведомлений пара обещание/будущее не работает.

одна заметка об этом примере-это задержки, добавленные в разных местах. Эти задержки были добавлены только для того, чтобы убедиться, что различные сообщения, напечатанные на консоли с помощью std::cout было бы ясно, и этот текст из нескольких потоков не будет перемешиваться.

первая часть main() создает три дополнительных потока и использует std::promise и std::future отправить данные между потоками. Интересным моментом является то, что основной поток запускает поток T2, который будет ждать данных из основного потока, что-то делать, а затем отправлять данные в третий поток T3, который затем что-то сделает и отправит данные обратно в основной поток.

вторая часть main() создает два потока и набор очередей, чтобы разрешить несколько сообщений из основного потока для каждого из двух созданных потоков. Мы не можем использовать std::promise и std::future для этого, потому что дуэт promise / future-это один выстрел и не может использоваться повторно.

источник для класса Sync_queue из языка программирования C++ Stroustrup: 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

обещание - это другой конец провода.

представьте, что вам нужно получить значение future вычисляется с помощью async. Однако вы не хотите, чтобы он вычислялся в одном потоке, и вы даже не создаете поток "сейчас" - возможно, ваше программное обеспечение было разработано для выбора потока из пула, поэтому вы не знаете кто выполнит вычисление che в конце.

теперь, что вы переходите к этому (пока неизвестно) поток / класс / сущность? Вы не проходите future, поскольку результат. Вы хотите передать что-то, что подключено до future что составляет другой конец провода, поэтому вы просто запросите future без знания о том, кто на самом деле вычислит/напишет что-то.

это promise. Это дескриптор подключен к вашей future. Если future Это динамик, и с get() вы начинаете слушать, пока не выйдет какой-то звук,promise это микрофон; но не просто микрофон, это на микрофон, соединенный одним проводом с динамиком, который вы держите. Вы можете знать, кто на другом конце провода, но вам это знать не обязательно - вы просто отдаете трубку и ждете, пока собеседник что-нибудь скажет.