Что такое 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
с и связанные future
s и имеют один поток, который выполняет несколько вычислений и устанавливает результат для каждого обещания. async
позволит вам вернуть только один результат, чтобы вернуть несколько вам нужно будет позвонить async
несколько раз, что может тратить ресурсы.
Теперь я немного лучше понимаю ситуацию (в немалой степени из-за ответов здесь!), Поэтому я подумал, что добавлю немного своих собственных заметок.
В C++11 есть два различных, хотя и связанных между собой понятия: асинхронное вычисление (функция, которая вызывается где-то еще) и параллельное выполнение (поток, то, что работает одновременно). Это несколько ортогональные понятия. Асинхронные вычисления - это просто другая разновидность вызова функции, а поток - это контекст выполнения. Потоки полезны сами по себе, но для целей этого обсуждения я буду рассматривать их как детали реализации.
Существует иерархия абстракций для асинхронных вычислений. Например, предположим, у нас есть функция, которая принимает несколько аргументов:
int foo(double, char, bool);
Во-первых, у нас есть шаблон std::future<T>
, который представляет собой будущее значение типа T
, Значение может быть получено через функцию-член get()
, который эффективно синхронизирует программу, ожидая результата. В качестве альтернативы, будущее поддерживает wait_for()
, который можно использовать для проверки того, что результат уже доступен. Фьючерсы следует рассматривать как асинхронную замену для обычных типов возвращаемых данных. Для нашего примера функции мы ожидаем std::future<int>
,
Теперь перейдем к иерархии, от высшего к низшему уровню:
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
Теперь мы можем рассмотреть, как реализовать что-то вроде
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
Теперь мы дошли до самого низкого уровня: как бы мы реализовали упакованную задачу? Это где
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 из них.
Основные вещи, которые вам нужны для асинхронной работы логики:
- Задача (логика упакована как некоторый объект функтора), который будет запускаться где-нибудь.
- Фактический узел обработки - поток, процесс и т. Д., Которые запускают такие функторы, когда они ему предоставляются. Посмотрите на шаблон проектирования "Command", чтобы понять, как это делает базовый пул рабочих потоков.
- Дескриптор результата: кому-то нужен этот результат, и ему нужен объект, который ПОЛУЧИТ его для них. По ООП и по другим причинам любое ожидание или синхронизацию следует выполнять в 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
это микрофон; но не просто микрофон, это микрофон, подключенный одним проводом к динамику, который вы держите. Вы можете знать, кто на другом конце, но вам не нужно это знать - вы просто даете это и ждете, пока другая сторона что-то скажет.