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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

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


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

Ответ 3

Бартош Милевски дает хорошую запись.

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

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

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

...

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

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

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Основными задачами, которые необходимо выполнить асинхронно, являются:

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

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

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

Ответ 7

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 для записи функциональности сопрограммы. См. Обсуждение и пример в fooobar.com/questions/13894458/... котором есть один раздел, в котором обсуждается использование 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 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

Ответ 8

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

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

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

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

Ответ 9

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