O que é std :: promise?

Eu estou bastante familiarizado com std::future componentes std::thread , std::async e std::future C ++ 11, por exemplo, veja essa resposta , que são diretos.

No entanto, não consigo entender o que é std::promise , o que ele faz e em quais situações ele é melhor usado. O documento padrão em si não contém muitas informações além de sua sinopse de class, e também não faz exatamente :: thread .

Alguém poderia dar um breve e sucinto exemplo de uma situação em que um std::promise é necessário e onde é a solução mais idiomática?

Nas palavras de [futures.state] um std::future é um object de retorno asynchronous (“um object que lê resultados de um estado compartilhado”) e um std::promise é um provedor asynchronous (“um object que fornece um resultado para um estado compartilhado “), ou seja, uma promise é a coisa em que você define um resultado, para que você possa obtê- lo do futuro associado.

O provedor asynchronous é o que inicialmente cria o estado compartilhado ao qual um futuro se refere. std::promise é um tipo de provedor asynchronous, std::packaged_task é outro, e o detalhe interno de std::async é outro. Cada um deles pode criar um estado compartilhado e fornecer a você um std::future que compartilha esse estado e pode deixar o estado pronto.

std::async é um utilitário de conveniência de nível mais alto que fornece um object de resultado asynchronous e, internamente, cuida da criação do provedor asynchronous e torna o estado compartilhado pronto quando a tarefa é concluída. Você pode emulá-lo com um std::packaged_task (ou std::bind e um std::promise ) e um std::thread mas é mais seguro e fácil de usar std::async .

std::promise é um pouco mais baixo, para quando você quer passar um resultado asynchronous para o futuro, mas o código que torna o resultado pronto não pode ser empacotado em uma única function adequada para passar para std::async . Por exemplo, você pode ter uma matriz de várias promise e future associados e ter um único encadeamento que faz vários cálculos e define um resultado em cada promise. async só permitiria que você retornasse um único resultado, para retornar vários, você precisaria chamar async várias vezes, o que poderia desperdiçar resources.

Eu entendo a situação um pouco melhor agora (não em pequena quantidade devido às respostas aqui!), Então eu pensei em adicionar um pequeno artigo meu.


Há dois conceitos distintos, embora relacionados, em C ++ 11: Computação assíncrona (uma function que é chamada em outro lugar) e execução simultânea (um thread , algo que funciona simultaneamente). Os dois são conceitos um pouco ortogonais. A computação assíncrona é apenas um sabor diferente da chamada de function, enquanto um thread é um contexto de execução. Tópicos são úteis por si só, mas para o propósito desta discussão, eu os tratarei como um detalhe de implementação.

Existe uma hierarquia de abstração para computação assíncrona. Por exemplo, suponha que tenhamos uma function que leve alguns argumentos:

 int foo(double, char, bool); 

Primeiramente, temos o template std::future , que representa um valor futuro do tipo T O valor pode ser recuperado através da function de membro get() , que efetivamente sincroniza o programa esperando pelo resultado. Como alternativa, um futuro suporta wait_for() , que pode ser usado para investigar se o resultado já está disponível ou não. Futuros devem ser considerados como substitutos asynchronouss para tipos de retorno comuns. Para nossa function de exemplo, esperamos um std::future .

Agora, para a hierarquia, do nível mais alto para o mais baixo:

  1. std::async : A maneira mais conveniente e direta de realizar uma computação assíncrona é através do modelo de function async , que retorna o futuro correspondente imediatamente:

     auto fut = std::async(foo, 1.5, 'x', false); // is a std::future 

    Nós temos muito pouco controle sobre os detalhes. Em particular, nem sabemos se a function é executada concorrentemente, serialmente em get() ou por alguma outra magia negra. No entanto, o resultado é facilmente obtido quando necessário:

     auto res = fut.get(); // is an int 
  2. Podemos agora considerar como implementar algo como async , mas de uma forma que nós controlamos. Por exemplo, podemos insistir que a function seja executada em um thread separado. Nós já sabemos que podemos fornecer um thread separado por meio da class std::thread .

    O próximo nível inferior de abstração faz exatamente isso: std::packaged_task . Este é um modelo que envolve uma function e fornece um futuro para o valor de retorno das funções, mas o object em si é chamado, e chamá-lo fica a critério do usuário. Podemos configurá-lo assim:

     std::packaged_task tsk(foo); auto fut = tsk.get_future(); // is a std::future 

    O futuro fica pronto quando chamamos a tarefa e a chamada é completada. Este é o trabalho ideal para um thread separado. Nós apenas temos que nos certificar de mover a tarefa para o tópico:

     std::thread thr(std::move(tsk), 1.5, 'x', false); 

    O encadeamento começa a ser executado imediatamente. Podemos detach lo ou join lo no final do escopo, ou sempre (por exemplo, usando o wrapper scoped_thread Anthony Williams, que realmente deve estar na biblioteca padrão). Os detalhes do uso do std::thread não nos interessam aqui; só não se esqueça de se juntar ou separar thr eventualmente. O que importa é que sempre que a chamada de function terminar, nosso resultado está pronto:

     auto res = fut.get(); // as before 
  3. Agora estamos no nível mais baixo: como podemos implementar a tarefa empacotada? É aí que entra o std::promise . A promise é o alicerce para se comunicar com um futuro. Os principais passos são estes:

    • O segmento de chamada faz uma promise.

    • O thread de chamada obtém um futuro da promise.

    • A promise, junto com os argumentos da function, é movida para um thread separado.

    • O novo segmento executa a function e preenche a promise.

    • O thread original recupera o resultado.

    Por exemplo, aqui está a nossa “tarefa empacotada”:

     template  class my_task; template  class my_task { std::function fn; std::promise pr; // the promise of the result public: template  explicit my_task(Ts &&... ts) : fn(std::forward(ts)...) { } template  void operator()(Ts &&... ts) { pr.set_value(fn(std::forward(ts)...)); // fulfill the promise } std::future get_future() { return pr.get_future(); } // disable copy, default move }; 

    O uso desse modelo é essencialmente o mesmo que o de std::packaged_task . Observe que a movimentação da tarefa inteira ocorre movendo a promise. Em situações mais ad-hoc, também é possível mover um object promise explicitamente para o novo encadeamento e torná-lo um argumento de function da function de encadeamento, mas um empacotador de tarefas como o acima parece uma solução mais flexível e menos intrusiva.


Fazendo exceções

Promessas estão intimamente relacionadas a exceções. A interface de uma promise, por si só, não é suficiente para transmitir completamente seu estado; assim, exceções são lançadas sempre que uma operação em uma promise não faz sentido. Todas as exceções são do tipo std::future_error , que deriva de std::logic_error . Primeiramente, uma descrição de algumas restrições:

  • Uma promise construída por padrão está inativa. Promessas inativas podem morrer sem conseqüências.

  • Uma promise torna-se ativa quando um futuro é obtido via get_future() . No entanto, apenas um futuro pode ser obtido!

  • Uma promise deve ser satisfeita via set_value() ou ter um conjunto de exceções via set_exception() antes que sua vida útil termine se seu futuro for consumido. Uma promise satisfeita pode morrer sem conseqüência e get() fica disponível no futuro. Uma promise com uma exceção aumentará a exceção armazenada após a chamada de get() no futuro. Se a promise não morrer nem com valor nem com exceção, chamar get() no futuro gerará uma exceção “promise quebrada”.

Aqui está uma pequena série de testes para demonstrar esses vários comportamentos excepcionais. Em primeiro lugar, o arnês:

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

Agora para os testes.

Caso 1: promise inativa

 int test() { std::promise pr; return 0; } // fine, no problems 

Caso 2: promise ativa, não utilizada

 int test() { std::promise pr; auto fut = pr.get_future(); return 0; } // fine, no problems; fut.get() would block indefinitely 

Caso 3: muitos futuros

 int test() { std::promise pr; auto fut1 = pr.get_future(); auto fut2 = pr.get_future(); // Error: "Future already retrieved" return 0; } 

Caso 4: promise satisfeita

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); } return fut.get(); } // Fine, returns "10". 

Caso 5: muita satisfação

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // Error: "Promise already satisfied" } return fut.get(); } 

A mesma exceção é lançada se houver mais de um dos valores de set_value ou set_exception .

Caso 6: Exceção

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo"))); } return fut.get(); } // throws the runtime_error exception 

Caso 7: promise quebrada

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); } // Error: "broken promise" return fut.get(); } 

Bartosz Milewski fornece um bom artigo.

C ++ divide a implementação de futuros em um conjunto de pequenos blocos

std :: promise é uma dessas partes.

Uma promise é um veículo para passar o valor de retorno (ou uma exceção) do encadeamento que executa uma function para o encadeamento que se encheckbox na function futura.

Um futuro é o object de synchronization construído em torno da extremidade de recepção do canal de promise.

Então, se você quiser usar um futuro, você acaba com uma promise que você usa para obter o resultado do processamento asynchronous.

Um exemplo da página é:

 promise intPromise; future intFuture = intPromise.get_future(); std::thread t(asyncFun, std::move(intPromise)); // do some other stuff int result = intFuture.get(); // may throw MyException 

Em uma aproximação aproximada, você pode considerar std::promise como o outro extremo de std::future (isso é falso , mas, por exemplo, você pode pensar como se fosse). O consumidor final do canal de comunicação usaria um std::future para consumir o dado do estado compartilhado, enquanto o segmento produtor usaria um std::promise para gravar no estado compartilhado.

std::promise é o canal ou caminho para a informação a ser retornada da function assíncrona. std::future é o mecanismo de synchronization que faz com que o chamador espere até que o valor de retorno transportado no std::promise esteja pronto (significando que seu valor é definido dentro da function).

Existem realmente 3 entidades principais no processamento asynchronous. C ++ 11 atualmente se concentra em dois deles.

As principais coisas que você precisa para executar alguma lógica de forma assíncrona são:

  1. A tarefa (lógica empacotada como algum object functor) que irá EXECUTAR ’em algum lugar’.
  2. O nó de processamento real – um encadeamento, um processo, etc., que executa esses functores quando são fornecidos a ele. Observe o padrão de design “Comando” para uma boa idéia de como um conjunto de threads de trabalho básico faz isso.
  3. A alça do resultado : alguém precisa desse resultado e precisa de um object para obtê-lo. Para OOP e outras razões, qualquer espera ou synchronization deve ser feita nas APIs desse identificador.

O C ++ 11 chama as coisas de que falo em (1) std::promise e aquelas em (3) std::future . std::thread é a única coisa oferecida publicamente para (2). Isso é lamentável porque os programas reais precisam gerenciar resources de thread e memory, e a maioria desejará executar tarefas em pools de threads em vez de criar e destruir um thread para cada pequena tarefa (o que quase sempre causa hits de desempenho desnecessários e pode facilmente criar resources fome que é ainda pior).

De acordo com Herb Sutter e outros no C ++ 11 brain trust, existem tentativas de adicionar um std::executor – assim como em Java – será a base para conjuntos de threads e configurações logicamente similares para (2). Talvez nós o vejamos em C ++ 2014, mas minha aposta é mais parecida com C ++ 17 (e Deus nos ajude se eles quebrarem o padrão para isso).

Um std::promise é criado como um ponto final para um par promise / futuro e o std::future (criado a partir do std :: promise usando o método get_future() ) é o outro ponto final. Esse é um método simples, com um único disparo, de fornecer um meio para dois encadeamentos serem sincronizados à medida que um encadeamento fornece dados para outro encadeamento por meio de uma mensagem.

Você pode pensar nisso como um segmento cria uma promise para fornecer dados e o outro segmento coleta a promise no futuro. Este mecanismo só pode ser usado uma vez.

O mecanismo promise / futuro é apenas uma direção, do encadeamento que usa o método set_value() de um std::promise para o encadeamento que usa o método get() de um std::future para receber os dados. Uma exceção é gerada se o método get() de um futuro for chamado mais de uma vez.

Se o thread com o std::promise não tiver usado set_value() para cumprir sua promise, quando o segundo thread chamar get() do std::future para coletar a promise, o segundo thread entrará em um estado de espera até que o promise é cumprida pelo primeiro segmento com o std::promise quando ele usa o método set_value() para enviar os dados.

O código de exemplo a seguir, um simples aplicativo de console do Windows do Visual Studio 2013, mostra o uso de algumas das classs / modelos de simultaneidade do C ++ 11 e outras funcionalidades. Ele ilustra um uso para promise / futuro que funciona bem, encadeamentos autônomos que farão algumas tarefas e serão interrompidos e um uso em que um comportamento mais síncrono é necessário e, devido à necessidade de várias notifications, o par prometo / futuro não funciona.

Uma nota sobre esse exemplo é os atrasos adicionados em vários lugares. Esses atrasos foram adicionados apenas para garantir que as várias mensagens impressas para o console usando std::cout fossem claras e que o texto dos diversos threads não fosse misturado.

A primeira parte do main() está criando três threads adicionais e usando std::promise e std::future para enviar dados entre os threads. Um ponto interessante é onde o encadeamento principal inicia um encadeamento, T2, que aguardará os dados do encadeamento principal, fará algo e enviará os dados para o terceiro encadeamento, T3, que fará então algo e enviará os dados de volta para o encadeamento principal. thread principal.

A segunda parte do main() cria dois encadeamentos e um conjunto de filas para permitir várias mensagens do encadeamento principal para cada um dos dois encadeamentos criados. Nós não podemos usar std::promise e std::future para isso porque o dueto promise / futuro é um tiro e não pode ser usado repetidamente.

A fonte para a class Sync_queue é da linguagem de programação C ++ do Stroustrup: 4ª edição.

 // cpp_threads.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include  #include  // std::thread is defined here #include  // std::future and std::promise defined here #include  // std::list which we use to build a message queue on. static std::atomic 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 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  q; // list that is used as a message queue }; template void Sync_queue::put(const Ttype &val) { std::lock_guard  lck(mtx); q.push_back(val); cond.notify_one(); } template void Sync_queue::get(Ttype &val) { std::unique_lock 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 &jj) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future 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 &jj, std::promise&pp) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future 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 &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 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 intPromiseT2; std::promise 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 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 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 q1; // notification queue for messages to thread t11 Sync_queue 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; } 

Este aplicativo simples cria a seguinte saída.

 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 

A promise é a outra extremidade do fio.

Imagine que você precisa recuperar o valor de um future sendo calculado por um async . No entanto, você não quer que ele seja computado no mesmo thread, e você nem sequer gera um thread “now” – talvez seu software tenha sido projetado para escolher um thread de um pool, então você não sabe quem será execute a computação final no final.

Agora, o que você passa para este (ainda desconhecido) segmento / class / entidade? Você não passa o future , pois este é o resultado . Você quer passar algo que esteja conectado ao future e que represente o outro lado do fio , então você apenas consultará o future sem saber quem realmente irá computar / escrever algo.

Essa é a promise . É um identificador conectado ao seu future . Se o future é um altofalante , e com get() você começa a ouvir até que algum som saia, a promise é um microfone ; mas não apenas qualquer microfone, é o microfone conectado com um único fio ao alto-falante que você segura. Você pode saber quem está do outro lado, mas você não precisa saber disso – basta dar e esperar até que a outra pessoa diga alguma coisa.

Intereting Posts