Qual é a diferença entre retornar nulo e retornar uma tarefa?

128

Ao examinar várias amostras CTP assíncronas do C #, vejo algumas funções assíncronas que retornam voide outras que retornam as não genéricas Task. Eu posso ver por que retornar a Task<MyType>é útil para retornar dados ao chamador quando a operação assíncrona é concluída, mas as funções que eu vi que têm um tipo de Taskretorno nunca retornam dados. Por que não voltar void?

James Cadd
fonte

Respostas:

214

As respostas de SLaks e Killercam são boas; Eu pensei em adicionar um pouco mais de contexto.

Sua primeira pergunta é essencialmente sobre quais métodos podem ser marcados async.

Um método marcado como asyncpode retornar void, Taskou Task<T>. Quais são as diferenças entre eles?

Um Task<T>método assíncrono de retorno pode ser aguardado e, quando a tarefa for concluída, ele oferecerá um T.

Um Taskmétodo assíncrono de retorno pode ser aguardado e, quando a tarefa for concluída, a continuação da tarefa está agendada para execução.

Um voidmétodo assíncrono retornado não pode ser aguardado; é um método "dispare e esqueça". Funciona de forma assíncrona e você não tem como saber quando isso é feito. Isso é mais do que um pouco estranho; como o SLaks diz, normalmente você faria isso apenas ao criar um manipulador de eventos assíncrono. O evento é disparado, o manipulador é executado; ninguém vai "aguardar" a tarefa retornada pelo manipulador de eventos, porque os manipuladores de eventos não retornam tarefas e, mesmo retornando, que código usaria a tarefa para alguma coisa? Geralmente, não é o código do usuário que transfere o controle para o manipulador em primeiro lugar.

Sua segunda pergunta, em um comentário, é essencialmente sobre o que pode ser awaiteditado:

Que tipos de métodos podem ser awaiteditados? Um método de retorno nulo pode ser awaiteditado?

Não, um método de retorno nulo não pode ser aguardado. O compilador se traduz await M()em uma chamada para M().GetAwaiter(), onde GetAwaiterpode ser um método de instância ou um método de extensão. O valor esperado deve ser aquele pelo qual você pode obter um garçom; claramente, um método de retorno nulo não produz um valor a partir do qual você pode obter um garçom.

Taskmétodos de retorno podem produzir valores esperáveis. Prevemos que terceiros desejarão criar suas próprias implementações de Taskobjetos semelhantes que podem ser aguardados, e você poderá aguardá-los. No entanto, você não poderá declarar asyncmétodos que retornam nada void, exceto , Taskou Task<T>.

(ATUALIZAÇÃO: Minha última frase pode ser falsificada por uma versão futura do C #; existe uma proposta para permitir tipos de retorno diferentes dos tipos de tarefas para métodos assíncronos.)

(ATUALIZAÇÃO: o recurso mencionado acima chegou ao C # 7.)

Eric Lippert
fonte
7
+1 Acho que a única coisa que falta é a diferença na maneira como as exceções são tratadas nos métodos assíncronos de retorno nulo.
João Angelo
10
@ JamesCadd: Suponha que algum trabalho assíncrono gere uma exceção. Quem pega isso? O código que iniciou a tarefa assíncrona não está mais na pilha - pode até não estar no mesmo encadeamento - e as exceções pressupõem que todos os blocos catch / finalmente estão na pilha . Então, o que você faz? Armazenamos as informações de exceção na Tarefa, para que você possa inspecioná-las mais tarde. Mas se o método for nulo retornando, não haverá Tarefa disponível para o código do usuário. Como lidamos exatamente com essa situação tem sido motivo de alguma controvérsia e não me lembro neste momento o que decidimos.
Eric Lippert
8
Na verdade, eu fiz essa pergunta de Stephen Toub no BUILD. No .NET 4.0 não observado, as exceções não tratadas no Tasks acabariam travando o processo assim que o TPL detectar que elas não foram observadas. No 4.5, eles mudaram o comportamento padrão para que as exceções não observadas ainda sejam relatadas por meio do evento TaskScheduler :: UnobservedTaskException, mas não travarão mais o processo. Se você deseja o antigo comportamento 4.0, pode voltar com <runtime> <ThrowUnobservedTaskExceptions enabled = "true" /> </runtime>. Provavelmente, a alteração foi feita precisamente para apoiar os métodos de disparar e esquecer para os métodos nulos assíncronos.
Drew Marsh
4
async voidOs métodos levantam sua exceção sobre o SynchronizationContextque estava ativo no momento em que começaram a executar. Isso é semelhante ao comportamento dos manipuladores de eventos (síncronos). @DrewMarsh: a UnobservedTaskExceptionconfiguração e o tempo de execução se aplicam apenas aos métodos de tarefa assíncrona "disparar e esquecer" , não aos async voidmétodos.
precisa
1
Link de citação para informações de manipulação de exceção assíncrona: blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx#11
Luke Puplett
23

Caso o chamador deseje aguardar a tarefa ou adicionar uma continuação.

De fato, o único motivo para retornar voidé se você não pode retornar Taskporque está escrevendo um manipulador de eventos.

SLaks
fonte
Eu pensei que era possível aguardar métodos que retornam um tipo vazio também - você poderia elaborar um pouco?
James Cadd
1
Não, você não pode. Se o método retornar void, você não poderá acessar a tarefa que ele gera. (Na verdade, eu não tenho certeza se ele mesmo gera um Taskem tudo)
SLaks
18

Os métodos retornando Taske Task<T>são composíveis - o que significa que você pode utilizá- awaitlos dentro de um asyncmétodo.

asyncOs métodos que retornam voidnão são composíveis, mas têm duas outras propriedades importantes:

  1. Eles podem ser usados ​​como manipuladores de eventos.
  2. Eles representam uma operação assíncrona de "nível superior".

O segundo ponto é importante quando você lida com um contexto que mantém uma contagem de operações assíncronas pendentes.

O contexto do ASP.NET é um desses contextos; se você usar Taskmétodos assíncronos sem aguardá-los de um voidmétodo assíncrono , a solicitação do ASP.NET será concluída muito cedo.

Outro contexto é o AsyncContextque escrevi para testes de unidade (disponível aqui ) - o AsyncContext.Runmétodo rastreia a contagem de operações pendentes e retorna quando é zero.

Stephen Cleary
fonte
12

Type Task<T>é o tipo de cavalo de trabalho da Task Parallel Library (TPL), representa o conceito de "algum trabalho / job que produzirá um resultado do tipo Tno futuro". O conceito de "trabalho que será concluído no futuro, mas não retornará resultado" é representado pelo tipo de tarefa não genérico.

Precisamente como o resultado do tipo Tserá produzido e os detalhes da implementação de uma tarefa específica; o trabalho pode ser distribuído para outro processo na máquina local, para outro encadeamento etc. As tarefas TPL são tipicamente distribuídas para encadeamentos de trabalho de um conjunto de encadeamentos no processo atual, mas esse detalhe da implementação não é fundamental para o Task<T>tipo; em vez disso, a Task<T>pode representar qualquer operação de alta latência que produza a T.

Com base no seu comentário acima:

A awaitexpressão significa "avaliar esta expressão para obter um objeto que representa um trabalho que no futuro produzirá um resultado. Registre o restante do método atual como retorno de chamada associado à continuação dessa tarefa. Depois que essa tarefa for produzida e a chamada de retorno estiver inscrito, retorne imediatamente o controle ao meu interlocutor ". Isso se opõe / contrasta com uma chamada de método regular, que significa "lembre-se do que você está fazendo, execute esse método até que esteja completamente concluído e, em seguida, continue de onde parou, agora sabendo o resultado do método".


Edit: Eu deveria citar o artigo de Eric Lippert em outubro de 2011 MSDN Magazine, pois isso foi uma grande ajuda para mim para entender essas coisas em primeiro lugar.

Para mais informações e páginas em branco, consulte aqui .

Espero que este seja de alguma ajuda.

Cavaleiro da Lua
fonte