Estou bastante familiarizado com C ++ 11 do std::thread
, std::async
e std::future
componentes (por exemplo, veja esta resposta ), que são simples e direta.
No entanto, não consigo entender bem o que std::promise
é, o que faz e em que situações é melhor usado. O documento padrão em si não contém muita informação além da sinopse de classe e nem apenas :: thread .
Alguém poderia, por favor, dar um exemplo sucinto e breve de uma situação em que std::promise
é necessária e onde é a solução mais idiomática?
c++
multithreading
c++11
promise
standard-library
Kerrek SB
fonte
fonte
std::promise
é de ondestd::future
vem.std::future
é o que permite recuperar um valor que lhe foi prometido . Quando você invocaget()
um futuro, ele espera até o proprietário da pessoastd::promise
com a qual define o valor (invocandoset_value
a promessa). Se a promessa for destruída antes que um valor seja definido e você invocarget()
um futuro associado a essa promessa, você receberá umastd::broken_promise
exceção porque lhe foi prometido um valor, mas é impossível obter um.std::broken_promise
é o identificador melhor nomeado na biblioteca padrão. E não hástd::atomic_future
.Respostas:
Nas palavras de [futures.state] a
std::future
é um objeto de retorno assíncrono ("um objeto que lê resultados de um estado compartilhado") e astd::promise
é um provedor assíncrono ("um objeto que fornece um resultado para um estado compartilhado"), ou seja, um promessa é a coisa em que você define um resultado, para que você possa obtê- lo no futuro associado.O provedor assíncrono é o que inicialmente cria o estado compartilhado ao qual um futuro se refere.
std::promise
é um tipo de provedor assíncrono,std::packaged_task
é outro, e os detalhes internos destd::async
é outro. Cada um deles pode criar um estado compartilhado e fornecer umstd::future
que compartilhe esse estado, além de poder prepará-lo.std::async
é um utilitário de conveniência de nível superior que fornece um objeto de resultado assíncrono e cuida internamente da criação do provedor assíncrono e da preparação do estado compartilhado quando a tarefa é concluída. Você pode emular com umstd::packaged_task
(oustd::bind
astd::promise
) e umstd::thread
mas é mais seguro e fácil de usarstd::async
.std::promise
é um pouco mais baixo, pois quando você deseja passar um resultado assíncrono para o futuro, mas o código que prepara o resultado não pode ser agrupado em uma única função adequada para a passagemstd::async
. Por exemplo, você pode ter uma matriz de vários sepromise
associadosfuture
e ter um único encadeamento que faz vários cálculos e define um resultado para cada promessa.async
só permitiria que você retornasse um único resultado; para retornar vários, você precisaria ligarasync
várias vezes, o que poderia desperdiçar recursos.fonte
future
é um exemplo concreto de um objeto de retorno assíncrono , que é um objeto que lê um resultado retornado de forma assíncrona, por meio do estado compartilhado. Apromise
é um exemplo concreto de um provedor assíncrono , que é um objeto que grava um valor no estado compartilhado, que pode ser lido de forma assíncrona. Eu quis dizer o que escrevi.Entendo a situação um pouco melhor agora (em grande parte devido às respostas aqui!), Então pensei em adicionar um pouco de minha própria redação.
Existem dois conceitos distintos, embora relacionados, no C ++ 11: Computação assíncrona (uma função que é chamada em outro lugar) e execução simultânea (um encadeamento , algo que funciona simultaneamente). Os dois são conceitos um tanto ortogonais. A computação assíncrona é apenas um tipo diferente de chamada de função, enquanto um encadeamento é um contexto de execução. Os threads são úteis por si só, mas para os fins 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 função que aceite alguns argumentos:
Primeiro, temos o modelo
std::future<T>
, que representa um valor futuro do tipoT
. O valor pode ser recuperado através da função membroget()
, que efetivamente sincroniza o programa, aguardando o resultado. Como alternativa, um futuro suportawait_for()
, que pode ser usado para verificar se o resultado já está disponível. Os futuros devem ser considerados como a substituição assíncrona de tipos de retorno comuns. Para nossa função de exemplo, esperamos astd::future<int>
.Agora, para a hierarquia, do nível mais alto para o mais baixo:
std::async
: A maneira mais conveniente e direta de executar uma computação assíncrona é através doasync
modelo de função, que retorna o futuro correspondente imediatamente:Temos muito pouco controle sobre os detalhes. Em particular, nem sabemos se a função é executada simultaneamente, em série
get()
ou por alguma outra magia negra. No entanto, o resultado é facilmente obtido quando necessário:Podemos agora considerar como implementar algo parecido
async
, mas de uma forma que nós controlar. Por exemplo, podemos insistir que a função seja executada em um thread separado. Já sabemos que podemos fornecer um thread separado por meio dastd::thread
classeO próximo nível mais baixo de abstração faz exatamente isso:
std::packaged_task
. Este é um modelo que envolve uma função e fornece um futuro para o valor de retorno das funções, mas o próprio objeto é passível de chamada e a chamada é a critério do usuário. Podemos configurá-lo assim:O futuro fica pronto quando chamamos a tarefa e a chamada é concluída. Esse é o trabalho ideal para um thread separado. Nós apenas temos que ter certeza de mover a tarefa para o thread:
O encadeamento começa a ser executado imediatamente. Podemos
detach
fazê-lo, ou tê-join
lo no final do escopo, ou sempre que necessário (por exemplo, usando oscoped_thread
wrapper de Anthony Williams , que realmente deveria estar na biblioteca padrão). Os detalhes do usostd::thread
não nos interessam aqui; apenas junte-se ou desanexe-othr
eventualmente. O que importa é que, sempre que a chamada da função termina, nosso resultado está pronto:Agora, estamos no nível mais baixo: como implementar a tarefa empacotada? É aqui que
std::promise
entra. A promessa é o alicerce para a comunicação com o futuro. As principais etapas são estas:O segmento de chamada faz uma promessa.
O segmento de chamada obtém um futuro da promessa.
A promessa, juntamente com os argumentos da função, são movidos para um thread separado.
O novo thread executa a função e cumpre a promessa.
O encadeamento original recupera o resultado.
Como exemplo, aqui está nossa "tarefa empacotada":
O uso desse modelo é essencialmente o mesmo que o de
std::packaged_task
. Observe que mover a tarefa inteira implica mover a promessa. Em situações mais ad-hoc, também é possível mover um objeto de promessa explicitamente para o novo encadeamento e torná-lo um argumento de função da função de encadeamento, mas um invólucro de tarefa como o descrito acima parece ser uma solução mais flexível e menos intrusiva.Fazendo exceções
Promessas estão intimamente relacionadas a exceções. A interface de uma promessa por si só não é suficiente para transmitir seu estado completamente; portanto, são lançadas exceções sempre que uma operação em uma promessa não faz sentido. Todas as exceções são do tipo
std::future_error
derivadas destd::logic_error
. Primeiro, uma descrição de algumas restrições:Uma promessa construída por padrão é inativa. Promessas inativas podem morrer sem conseqüências.
Uma promessa se torna ativa quando um futuro é obtido via
get_future()
. No entanto, apenas um futuro pode ser obtido!Uma promessa deve ser cumprida via
set_value()
ou ter uma exceção definidaset_exception()
antes de sua vida útil terminar, para que seu futuro seja consumido. Uma promessa satisfeita pode morrer sem conseqüências eget()
se tornar disponível no futuro. Uma promessa com uma exceção aumentará a exceção armazenada quando solicitadoget()
no futuro. Se a promessa morrer sem valor nem exceção, o conviteget()
ao futuro criará uma exceção de "promessa quebrada".Aqui está uma pequena série de testes para demonstrar esses vários comportamentos excepcionais. Primeiro, o chicote:
Agora vamos aos testes.
Caso 1: promessa inativa
Caso 2: promessa ativa, não utilizada
Caso 3: Muitos futuros
Caso 4: promessa cumprida
Caso 5: Muita satisfação
A mesma exceção é lançada se houver mais do que um de qualquer de
set_value
ouset_exception
.Caso 6: Exceção
Caso 7: Promessa quebrada
fonte
std::function
tem muitos construtores; Não há razão para não expô-los ao consumidor demy_task
.get()
no futuro gera uma exceção. Vou esclarecer isso adicionando "antes que seja destruído"; informe-me se isso estiver suficientemente claro.got()
minhafuture
de grokking a biblioteca de suporte linha nopromise
de sua explicação incrível!Bartosz Milewski fornece uma boa descrição.
std :: promessa é uma dessas partes.
...
Portanto, se você quiser usar um futuro, terá a promessa de obter o resultado do processamento assíncrono.
Um exemplo da página é:
fonte
Em uma aproximação aproximada, você pode considerar
std::promise
o outro extremo de astd::future
(isso é falso , mas para ilustração, você pode pensar como se fosse). O consumidor final do canal de comunicação usaria astd::future
para consumir o dado do estado compartilhado, enquanto o encadeamento produtor usaria astd::promise
para gravar no estado compartilhado.fonte
std::async
pode conceitualmente (isso não é obrigatório pelo padrão) entender como uma função que cria astd::promise
, envia isso para um pool de threads (das sortes, pode ser um pool de threads, pode ser um novo thread, ...) e retorna o associadostd::future
ao chamador. No lado do cliente, você esperaria nostd::future
e um thread do outro lado calcularia o resultado e o armazenaria nostd::promise
. Nota: o padrão requer o estado compartilhado estd::future
a existência, mas não a, de umstd::promise
neste caso de uso específico.std::future
não chamajoin
o thread, ele possui um ponteiro para um estado compartilhado, que é o buffer de comunicação real. O estado compartilhado possui um mecanismo de sincronização (provavelmentestd::function
+std::condition_variable
para bloquear o chamador até que o processostd::promise
seja concluído. A execução do encadeamento é ortogonal a tudo isso e, em muitas implementações, você pode achar questd::async
não são executados por novos encadeamentos que são unidos, mas sim por um pool de segmentos cuja vida útil se estende até o final do programa.std::async
é que a biblioteca de tempo de execução pode tomar as decisões corretas para você em relação ao número de threads a serem criados e, na maioria dos casos, esperarei tempos de execução que usem conjuntos de threads. Atualmente, o VS2012 usa um pool de threads sob o capô e não viola a regra como se . Observe que há muito poucas garantias que precisam ser cumpridas para esse particular como se .std::promise
é o canal ou caminho para que as informações sejam retornadas da função assíncrona.std::future
é o mecanismo de sincronização que faz o chamador esperar até que o valor de retorno carregado nostd::promise
esteja pronto (ou seja, seu valor é definido dentro da função).fonte
Existem realmente três entidades principais no processamento assíncrono. O C ++ 11 atualmente se concentra em 2 deles.
As principais coisas que você precisa para executar alguma lógica de forma assíncrona são:
O C ++ 11 chama as coisas de que falo em (1)
std::promise
e as de (3)std::future
.std::thread
é a única coisa fornecida publicamente (2). Isso é lamentável, porque programas reais precisam gerenciar recursos de encadeamento e memória, e a maioria deseja que as tarefas sejam executadas em conjuntos de encadeamentos, em vez de criar e destruir um encadeamento para cada pequena tarefa (que quase sempre causa resultados de desempenho desnecessários por si só e pode facilmente criar recursos fome ainda pior).De acordo com Herb Sutter e outros membros do C ++ 11 brain trust, existem planos preliminares de adicionar algo
std::executor
como o Java - será a base para pools de threads e configurações logicamente semelhantes para (2). Talvez o veremos no C ++ 2014, mas minha aposta é mais parecida com o C ++ 17 (e Deus nos ajude se eles não cumprirem o padrão).fonte
A
std::promise
é criado como um ponto final para um par promessa / futuro e ostd::future
(criado a partir do std :: promessa usando oget_future()
método) é o outro ponto final. Esse é um método simples e único de fornecer uma maneira de sincronizar dois threads, pois um thread fornece dados para outro thread por meio de uma mensagem.Você pode pensar nisso como um segmento cria uma promessa de fornecer dados e o outro segmento coleta a promessa no futuro. Este mecanismo pode ser usado apenas uma vez.
O mecanismo de promessa / futuro é apenas uma direção, do encadeamento que usa o
set_value()
método destd::promise
a ao encadeamento que usa oget()
de astd::future
para receber os dados. Uma exceção será gerada se oget()
método de um futuro for chamado mais de uma vez.Se o segmento com o
std::promise
não tiver sido usadoset_value()
para cumprir sua promessa, quando o segundo segmento chamarget()
ostd::future
para coletar a promessa, o segundo segmento entrará em um estado de espera até que a promessa seja cumprida pelo primeiro segmento estd::promise
quando ele usar oset_value()
método para enviar os dados.Com as corotinas propostas das Linguagens de Programação da Especificação Técnica N4663 - Extensões C ++ para Coroutines e o suporte do compilador C ++ do Visual Studio 2017
co_await
, também é possível usarstd::future
estd::async
gravar a funcionalidade da corotina. Consulte a discussão e o exemplo em https://stackoverflow.com/a/50753040/1466970, que possui uma seção que discute o uso destd::future
withco_await
.O código de exemplo a seguir, um aplicativo simples do console do Visual Studio 2013 para Windows, mostra o uso de algumas das classes / modelos de simultaneidade C ++ 11 e outras funcionalidades. Ilustra um uso para promessa / futuro que funciona bem, threads autônomos que executam algumas tarefas e param, e um uso em que é necessário um comportamento mais síncrono e devido à necessidade de várias notificações, o par promessa / futuro não funciona.
Uma observação sobre este exemplo são os atrasos adicionados em vários locais. Esses atrasos foram adicionados apenas para garantir que as várias mensagens impressas no console
std::cout
fossem claras e que o texto dos vários segmentos não seria entremeado.A primeira parte
main()
é criar três threads adicionais e usarstd::promise
estd::future
enviar dados entre os threads. Um ponto interessante é onde o encadeamento principal inicia um encadeamento, T2, que aguarda os dados do encadeamento principal, faz alguma coisa e envia os dados para o terceiro encadeamento, T3, que faz algo e envia os dados de volta para o encadeamento. rosca principal.A segunda parte do
main()
cria dois threads e um conjunto de filas para permitir várias mensagens do thread principal para cada um dos dois threads criados. Nós não podemos usarstd::promise
estd::future
para isso, porque a promessa / futuro duo são um disparo e não pode ser usado repetidamente.A fonte da classe
Sync_queue
é da The C ++ Programming Language: 4th Edition, da Stroustrup.Este aplicativo simples cria a seguinte saída.
fonte
A promessa é a outra extremidade do fio.
Imagine que você precisa recuperar o valor de um
future
ser calculado por umasync
. No entanto, você não deseja que ele seja computado no mesmo encadeamento e nem gera um encadeamento "agora" - talvez seu software tenha sido projetado para escolher um encadeamento de um pool, para que você não saiba quem irá execute a computação no final.Agora, o que você passa para esse thread / classe / entidade (ainda desconhecida)? Você não passa
future
, pois esse é o resultado . Você deseja passar algo que esteja conectado aofuture
e que represente a outra extremidade do fio , portanto, você apenas consultará ofuture
sem conhecimento sobre quem realmente calculará / gravará algo.Este é o
promise
. É uma alça conectada ao seufuture
. Sefuture
for um alto - falante , e comget()
você começar a ouvir até que algum som saia,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 não precisa saber - basta dar e esperar até que a outra parte diga alguma coisa.fonte
http://www.cplusplus.com/reference/future/promise/
Uma explicação da frase: furture :: get () espera promse :: set_value () para sempre.
fonte