assíncrono / aguardar - quando retornar uma tarefa vs anular?


503

Em que cenários alguém gostaria de usar

public async Task AsyncMethod(int num)

ao invés de

public async void AsyncMethod(int num)

O único cenário em que consigo pensar é se você precisa da tarefa para acompanhar seu progresso.

Além disso, no método a seguir, são assíncronas e aguardam palavras-chave desnecessárias?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
Note-se que métodos assíncronos deve sempre ser o sufixo com o Async.Example nome Foo()se tornaria FooAsync().
Fred

30
@ Fred Principalmente, mas nem sempre. Esta é apenas a convenção e as exceções aceitas a esta convenção são com classes baseadas em eventos ou contratos de interface, consulte MSDN . Por exemplo, você não deve renomear manipuladores de eventos comuns, como Button1_Click.
Ben

14
Apenas uma nota que você não deve usar Thread.Sleepcom suas tarefas você deve await Task.Delay(num)em vez
Bob Vale

45
@fred Eu não concordo com isso, a IMO adicionar um sufixo assíncrono só deve ser usada quando você estiver fornecendo uma interface com as opções de sincronização e assíncrona. Smurf nomear coisas com assíncrono quando houver apenas uma intenção é inútil. O caso em questão Task.Delaynão é Task.AsyncDelaycomo todos os métodos da tarefa são assíncronos
Não é amado

11
Eu tive um problema interessante esta manhã com um método de controlador webapi 2, que foi declarado como async voidalternativa async Task. O método travou porque estava usando um objeto de contexto do Entity Framework declarado como membro do controlador antes que o método fosse executado. A estrutura descartou o controlador antes que seu método fosse executado. Mudei o método para assíncrono de tarefas e funcionou.
costa

Respostas:


417

1) Normalmente, você gostaria de retornar a Task. A principal exceção deve ser quando você precisa ter um voidtipo de retorno (para eventos). Se não há motivo para não permitir que o chamador seja awaitsua tarefa, por que não?

2) os asyncmétodos que retornam voidsão especiais em outro aspecto: eles representam operações assíncronas de nível superior e têm regras adicionais que entram em jogo quando sua tarefa retorna uma exceção. A maneira mais fácil é mostrar a diferença com um exemplo:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fA exceção de sempre é "observada". Uma exceção que deixa um método assíncrono de nível superior é simplesmente tratada como qualquer outra exceção não tratada. gA exceção de nunca é observada. Quando o coletor de lixo vem para limpar a tarefa, ele vê que a tarefa resultou em uma exceção e ninguém tratou da exceção. Quando isso acontece, o TaskScheduler.UnobservedTaskExceptionmanipulador é executado. Você nunca deve deixar isso acontecer. Para usar seu exemplo,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Sim, use asynce awaitaqui, eles garantem que seu método ainda funcione corretamente se uma exceção for lançada.

para obter mais informações, consulte: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
Eu quis dizer em fvez de gno meu comentário. A exceção de fé passada para o SynchronizationContext. gaumentará UnobservedTaskException, mas UTEnão interromperá mais o processo se não for tratado. Há algumas situações em que é aceitável ter "exceções assíncronas" como essa que são ignoradas.
Stephen Cleary

3
Se você tiver WhenAnyvários Tasks resultando em exceções. Muitas vezes, você só precisa lidar com o primeiro e muitas vezes deseja ignorar os outros.
Stephen Cleary

1
@StephenCleary Obrigado, acho que esse é um bom exemplo, embora dependa do motivo pelo qual você ligou WhenAnyem primeiro lugar para ignorar as outras exceções. , com ou sem uma exceção.

10
Estou um pouco confuso sobre o motivo pelo qual você recomenda devolver uma tarefa em vez de nula. Como você disse, f () irá lançar e exceção, mas g () não. Não é melhor tomar conhecimento dessas exceções de encadeamento em segundo plano?
user981225

2
@ user981225 De fato, mas isso passa a ser responsabilidade do chamador de g: todo método que chama g deve ser assíncrono e usar aguardar também. Esta é uma diretriz, não uma regra difícil, você pode decidir que, em seu programa específico, é mais fácil ter um retorno n vazio.

40

Encontrei este artigo muito útil sobre asynce voidescrito por Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

A linha inferior é que um async+voidpode travar o sistema e geralmente deve ser usado apenas nos manipuladores de eventos do lado da interface do usuário.

A razão por trás disso é o Contexto de Sincronização usado pelo AsyncVoidMethodBuilder, sendo nenhum neste exemplo. Quando não há contexto de sincronização de ambiente, qualquer exceção não tratada pelo corpo de um método de async void é novamente mostrada no ThreadPool. Embora aparentemente não haja outro local lógico em que esse tipo de exceção não tratada possa ser lançada, o efeito infeliz é que o processo está sendo encerrado, porque as exceções não tratadas no ThreadPool encerram o processo efetivamente desde o .NET 2.0. Você pode interceptar todas as exceções não tratadas usando o evento AppDomain.UnhandledException, mas não há como recuperar o processo desse evento.

Ao escrever manipuladores de eventos da interface do usuário, os métodos assíncronos nulos são indolores porque as exceções são tratadas da mesma maneira encontrada nos métodos não assíncronos; eles são lançados no Dispatcher. Existe a possibilidade de se recuperar dessas exceções, pois é mais do que correto na maioria dos casos. No entanto, fora dos manipuladores de eventos da interface do usuário, os métodos assíncronos nulos são perigosos de usar e podem não ser fáceis de encontrar.


Isso está correto? Eu pensei que exceções dentro de métodos assíncronos são capturadas na tarefa. Além disso, sempre há um deafault SynchronizationContext
NM

30

Eu tive uma ideia clara dessas declarações.

  1. Os métodos assíncronos nulos têm diferentes semânticas de tratamento de erros. Quando uma exceção é lançada de um método de tarefa assíncrona ou tarefa assíncrona, essa exceção é capturada e colocada no objeto Tarefa. Com os métodos async void, não há objeto Task, portanto, as exceções lançadas fora de um método async void serão geradas diretamente no SynchronizationContext (SynchronizationContext representa um local "onde" o código pode ser executado.) Que estava ativo quando o método async void começado

Exceções de um método de vácuo assíncrono não podem ser capturadas com captura

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Essas exceções podem ser observadas usando AppDomain.UnhandledException ou um evento abrangente para aplicativos GUI / ASP.NET, mas o uso desses eventos para o tratamento regular de exceções é uma receita para a impossibilidade de manutenção (ele trava o aplicativo).

  1. Os métodos de vácuo assíncrono têm diferentes semânticas de composição. Os métodos assíncronos que retornam Tarefa ou Tarefa podem ser facilmente compostos usando Aguardar, Task.WhenAny, Task.WhenAll e assim por diante. Os métodos assíncronos que retornam nulo não fornecem uma maneira fácil de notificar o código de chamada que eles concluíram. É fácil iniciar vários métodos de cancelamento assíncrono, mas não é fácil determinar quando eles terminaram. Os métodos Async void notificarão seu SynchronizationContext quando iniciarem e terminarem, mas um SynchronizationContext personalizado é uma solução complexa para código de aplicativo comum.

  2. O método Async Void é útil ao usar o manipulador de eventos síncronos, porque eles geram suas exceções diretamente no SynchronizationContext, que é semelhante ao comportamento dos manipuladores de eventos síncronos

Para obter mais detalhes, consulte este link https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

O problema de chamar async void é que você nem recebe a tarefa de volta; não há como saber quando a tarefa da função foi concluída (consulte https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Aqui estão as três maneiras de chamar uma função assíncrona:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

Em todos os casos, a função é transformada em uma cadeia de tarefas. A diferença é o que a função retorna.

No primeiro caso, a função retorna uma tarefa que eventualmente produz ot.

No segundo caso, a função retorna uma tarefa que não tem produto, mas você ainda pode esperar para saber quando foi concluída.

O terceiro caso é o desagradável. O terceiro caso é como o segundo, exceto que você nem recebe a tarefa de volta. Você não tem como saber quando a tarefa da função foi concluída.

O caso de async void é um "acenda e esqueça": você inicia a cadeia de tarefas, mas não se importa quando termina. Quando a função retorna, tudo que você sabe é que tudo, até o primeiro momento de espera, foi executado. Tudo após a primeira espera será executado em algum momento não especificado no futuro ao qual você não tem acesso.


6

Acho que você também pode usar async voidpara iniciar operações em segundo plano, desde que tenha cuidado para capturar exceções. Pensamentos?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
O problema com a chamada assíncrona vazio é que você nem sequer chegar a tarefa de volta, você não tem nenhuma maneira de saber quando a tarefa da função completou
user8128167

Isso faz sentido para (raras) operações em segundo plano nas quais você não se importa com o resultado e não deseja que uma exceção afete outras operações. Um caso de uso típico seria enviar um evento de log para um servidor de log. Você deseja que isso ocorra em segundo plano e não deseja que o manipulador de serviço / solicitação falhe se o servidor de log estiver inativo. Obviamente, você precisa capturar todas as exceções, ou seu processo será encerrado. Ou existe uma maneira melhor de conseguir isso?
Florian Winter

Exceções de um método de vácuo assíncrono não podem ser capturadas com captura. msdn.microsoft.com/pt-br/magazine/jj991977.aspx
Si Zi

3
@SiZi, a captura está DENTRO do método de async void, como mostrado no exemplo, e é capturada.
bboyle1234

E quando seus requisitos mudam para exigir que não funcionem se você não puder registrá-lo - é necessário alterar as assinaturas em toda a API, em vez de apenas alterar a lógica que atualmente possui // Ignore Exception
Milney

0

Minha resposta é simples, você não pode esperar método nulo

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Portanto, se o método for assíncrono, é melhor aguardar, pois você pode perder a vantagem assíncrona.


-2

De acordo com a documentação da Microsoft , NUNCA deve usarasync void

Não faça isso: O exemplo a seguir usa o async voidque torna a solicitação HTTP concluída quando a primeira espera é atingida:

  • O que é SEMPRE uma má prática nos aplicativos ASP.NET Core.

  • Acessa o HttpResponse após a conclusão da solicitação HTTP.

  • Trava o processo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.