Qualquer diferença entre “await Task.Run (); Retorna;" e “return Task.Run ()”?

90

Existe alguma diferença conceitual entre os dois trechos de código a seguir:

async Task TestAsync() 
{
    await Task.Run(() => DoSomeWork());
}

e

Task TestAsync() 
{
    return Task.Run(() => DoSomeWork());
}

O código gerado é diferente?

EDITAR: Para evitar confusão com Task.Run, um caso semelhante:

async Task TestAsync() 
{
    await Task.Delay(1000);
}

e

Task TestAsync() 
{
    return Task.Delay(1000);
}

ATRASO: Além da resposta aceita, também há uma diferença em como LocalCallContexté tratado: CallContext.LogicalGetData é restaurado mesmo onde não há assincronia. Por quê?

evitar
fonte
1
Sim, é diferente. E isso difere muito. caso contrário, não haveria sentido em usar await/ asyncem absoluto :)
MarcinJuraszek
1
Acho que há duas questões aqui. 1. A implementação real do método é importante para o responsável pela chamada? 2. As representações compiladas dos dois métodos são diferentes?
DavidRR

Respostas:

81

Uma das principais diferenças está na propagação de exceções. Uma excepção, jogado dentro de um async Taskmétodo, é armazenado na devolvido Taskobjecto e permanece dormente até que a tarefa fica observada através de await task, task.Wait(), task.Resultou task.GetAwaiter().GetResult(). Ele é propagado dessa forma, mesmo se lançado da parte síncrona do asyncmétodo.

Considere o seguinte código, onde OneTestAsynce AnotherTestAsyncse comporta de maneira bastante diferente:

static async Task OneTestAsync(int n)
{
    await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
    return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
    Task task = null;
    try
    {
        // start the task
        task = whatTest(n);

        // do some other stuff, 
        // while the task is pending
        Console.Write("Press enter to continue");
        Console.ReadLine();
        task.Wait();
    }
    catch (Exception ex)
    {
        Console.Write("Error: " + ex.Message);
    }
}

Se eu chamar DoTestAsync(OneTestAsync, -2), ele produzirá a seguinte saída:

Pressione Enter para continuar
Erro: um ou mais erros ocorreram.await Task.Delay
Erro: 2º

Note, eu tive que pressionar Enterpara ver.

Agora, se eu chamar DoTestAsync(AnotherTestAsync, -2), o fluxo de trabalho do código interno DoTestAsyncé bastante diferente, assim como a saída. Desta vez, não fui solicitado a pressionar Enter:

Erro: o valor precisa ser -1 (significando um tempo limite infinito), 0 ou um número inteiro positivo.
Nome do parâmetro: millisecondsDelayError: 1st

Em ambos os casos Task.Delay(-2) joga no início, ao validar seus parâmetros. Este pode ser um cenário inventado, mas em teoria Task.Delay(1000)pode lançar também, por exemplo, quando a API do cronômetro do sistema subjacente falha.

Em uma nota lateral, a lógica de propagação de erro ainda é diferente para async voidmétodos (em oposição aos async Taskmétodos). Uma exceção levantada dentro de um async voidmétodo será imediatamente relançada no contexto de sincronização do thread atual (via SynchronizationContext.Post), se o thread atual tiver um (SynchronizationContext.Current != null) . Caso contrário, ela será relançada via ThreadPool.QueueUserWorkItem). O chamador não tem chance de lidar com essa exceção no mesmo quadro de pilha.

Publiquei mais alguns detalhes sobre o comportamento de tratamento de exceções TPL aqui e aqui .


P : É possível simular o comportamento de propagação de exceções de asyncmétodos para métodos não-assíncronos Task, de modo que o último não atinja o mesmo frame de pilha?

R : Se realmente necessário, então sim, há um truque para isso:

// async
async Task<int> MethodAsync(int arg)
{
    if (arg < 0)
        throw new ArgumentException("arg");
    // ...
    return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
    var task = new Task<int>(() => 
    {
        if (arg < 0)
            throw new ArgumentException("arg");
        // ...
        return 42 + arg;
    });

    task.RunSynchronously(TaskScheduler.Default);
    return task;
}

No entanto, observe que, sob certas condições (como quando é muito profundo na pilha), RunSynchronouslyainda pode ser executado de forma assíncrona.


Outra diferença notável é que a versão async/ awaité mais propensa a travamento em um contexto de sincronização não padrão . Por exemplo, o seguinte irá travar em um aplicativo WinForms ou WPF:

static async Task TestAsync()
{
    await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
    TestAsync().Wait(); // dead-lock here
}

Altere para uma versão não assíncrona e não bloqueará:

Task TestAsync() 
{
    return Task.Delay(1000);
}

A natureza do bloqueio é bem explicada por Stephen Cleary em seu blog .

noseratio
fonte
2
Acredito que o deadlock no primeiro exemplo poderia ser evitado adicionando .ConfigureAwait (false) à linha de espera, pois isso só acontece porque o método está tentando retornar ao mesmo contexto de execução. Portanto, as exceções são a única diferença que permanece.
relativamente
2
@relativamente_random, seu comentário está correto, embora a resposta seja sobre a diferença entre return Task.Run()e await Task.Run(); return, em vez deawait Task.Run().ConfigureAwait(false); return
noseratio
Se você descobrir que o programa fecha após pressionar Enter, certifique-se de pressionar ctrl + F5 em vez de F5.
David Klempfner
53

Qual é a diferença entre

async Task TestAsync() 
{
    await Task.Delay(1000);
}

e

Task TestAsync() 
{
    return Task.Delay(1000);
}

?

Estou confuso com esta pergunta. Deixe-me tentar esclarecer respondendo à sua pergunta com outra pergunta. Qual é a diferença entre?

Func<int> MakeFunction()
{
    Func<int> f = ()=>1;
    return ()=>f();
}

e

Func<int> MakeFunction()
{
    return ()=>1;
}

?

Qualquer que seja a diferença entre minhas duas coisas, a mesma diferença é entre as suas duas coisas.

Eric Lippert
fonte
23
Claro! Você abriu meus olhos :) No primeiro caso, crio uma tarefa de wrapper, semanticamente próxima de Task.Delay(1000).ContinueWith(() = {}). No segundo, é só Task.Delay(1000). A diferença é um tanto sutil, mas significativa.
avo
3
Você poderia explicar um pouco a diferença? na verdade eu não ... Obrigado
zheng yu
4
Dado que há uma diferença sutil com contextos de sincronização e propagação de exceção, eu diria que a diferença entre async / await e wrappers de função não é a mesma.
Cameron MacFarland
1
@CameronMacFarland: É por isso que pedi esclarecimentos. A questão que se coloca é se há uma diferença conceitual entre os dois. Bem, eu não sei. Certamente existem muitas diferenças; alguma delas conta como diferenças "conceituais"? Em meu exemplo com funções aninhadas, também existem diferenças na propagação de erros; se as funções forem encerradas no estado local, haverá diferenças nos tempos de vida locais e assim por diante. Essas diferenças são "conceituais"?
Eric Lippert
8
Esta é uma resposta antiga, mas acredito que dada hoje, teria sido reprovada. Não responde à pergunta, nem aponta o OP para uma fonte da qual ele possa aprender.
Daniel Dubovski de
11
  1. O primeiro método nem mesmo compila.

    Como ' Program.TestAsync()' é um método assíncrono que retorna ' Task', uma palavra-chave de retorno não deve ser seguida por uma expressão de objeto. Você pretendia voltar ' Task<T>'?

    Tem que ser

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Há uma grande diferença conceitual entre esses dois. O primeiro é assíncrono, o segundo não. Leia Desempenho do Async: Compreendendo os custos do Async e Await para saber um pouco mais sobre os aspectos internos de async/ await.

  3. Eles geram códigos diferentes.

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
            01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
            2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
            79 6e 63 3e 64 5f 5f 31 00 00
        )
        .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x216c
        // Code size 62 (0x3e)
        .maxstack 2
        .locals init (
            [0] valuetype SOTestProject.Program/'<TestAsync>d__1',
            [1] class [mscorlib]System.Threading.Tasks.Task,
            [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
        )
    
        IL_0000: ldloca.s 0
        IL_0002: ldarg.0
        IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
        IL_0008: ldloca.s 0
        IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
        IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0014: ldloca.s 0
        IL_0016: ldc.i4.m1
        IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
        IL_001c: ldloca.s 0
        IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0023: stloc.2
        IL_0024: ldloca.s 2
        IL_0026: ldloca.s 0
        IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
        IL_002d: ldloca.s 0
        IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
        IL_0039: stloc.1
        IL_003a: br.s IL_003c
    
        IL_003c: ldloc.1
        IL_003d: ret
    } // end of method Program::TestAsync
    

    e

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed 
    {
        // Method begins at RVA 0x21d8
        // Code size 23 (0x17)
        .maxstack 2
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
        IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
        IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
        IL_0012: stloc.0
        IL_0013: br.s IL_0015
    
        IL_0015: ldloc.0
        IL_0016: ret
    } // end of method Program::TestAsync2
    
Marcin Juraszek
fonte
@MarcinJuraszek, na verdade ele não compilou. Isso foi um erro de digitação, tenho certeza de que você acertou. Caso contrário, uma ótima resposta, obrigado! Achei que C # poderia ser inteligente o suficiente para evitar a geração de uma classe de máquina de estado no primeiro caso.
avo
9

Os dois exemplos são diferentes. Quando um método é marcado com a asyncpalavra - chave, o compilador gera uma máquina de estado nos bastidores. Este é o responsável por retomar as continuações uma vez que um aguardado tenha sido aguardado.

Em contraste, quando um método não é marcado com asyncvocê está perdendo a capacidade de awaitesperar. (Ou seja, dentro do próprio método; o método ainda pode ser aguardado por seu chamador.) No entanto, evitando oasync palavra chave, você não está mais gerando a máquina de estado, que pode adicionar um pouco de sobrecarga (elevar locais para campos da máquina de estado, objetos adicionais ao GC).

Em exemplos como este, se você for capaz de evitar async-await e retornar um objeto de espera diretamente, isso deve ser feito para melhorar a eficiência do método.

Veja esta pergunta e esta resposta que são muito semelhantes à sua pergunta e esta resposta.

Lukazoid
fonte