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


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

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

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

8 325

8 ответов:

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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