Что такое STD:: Обещание?

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

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

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

8 ответов

Решение

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

Асинхронный поставщик - это то, что изначально создает общее состояние, к которому относится будущее. 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, Например, вы можете иметь массив из нескольких promiseс и связанные futures и имеют один поток, который выполняет несколько вычислений и устанавливает результат для каждого обещания. async позволит вам вернуть только один результат, чтобы вернуть несколько вам нужно будет позвонить async несколько раз, что может тратить ресурсы.

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


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


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

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 приходит. Обещание является строительным блоком для общения с будущим. Основными шагами являются следующие:

    • Вызывающая нить дает обещание.

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

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

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

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

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

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


Делать исключения

Обещания тесно связаны с исключениями. Одного интерфейса обещания недостаточно, чтобы полностью передать его состояние, поэтому создаются исключения, когда операция с обещанием не имеет смысла. Все исключения имеют тип 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();
}

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

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

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

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

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

С предложенными сопрограммами Технической спецификации N4663 Языки программирования - расширения C++ для сопрограмм и поддержка компилятора Visual Studio 2017 C++: co_await, также можно использовать std::future а также std::async написать сопрограмму функциональности. См. Обсуждение и пример в /questions/25035448/potoki-c11-dlya-obnovleniya-okon-prilozhenij-mfc-sendmessage-postmessage-trebuetsya/25035450#25035450 котором в качестве одного раздела рассматривается использование std::future с co_await,

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

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

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

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

Источник для класса Sync_queue взято из Страуструпа Язык программирования C++: 4-е издание.

// 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

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

Основные вещи, которые вам нужны для асинхронной работы логики:

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

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

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

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

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

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

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

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

Другие вопросы по тегам