Qualsiasi differenza tra "await Task.Run (); ritorno;" e "return Task.Run ()"?


90

C'è qualche differenza concettuale tra i seguenti due pezzi di codice:

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

e

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

Anche il codice generato è diverso?

EDIT: per evitare confusione con Task.Runun caso simile:

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

e

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

AGGIORNAMENTO TARDIVO: oltre alla risposta accettata, c'è anche una differenza nel modo in cui LocalCallContextviene gestito: CallContext.LogicalGetData viene ripristinato anche dove non c'è asincronia. Perché?


1
Sì, è diverso. E differisce molto. altrimenti non avrebbe senso usare await/ asyncaffatto :)
MarcinJuraszek

1
Penso che ci siano due domande qui. 1. L'implementazione effettiva del metodo è importante per il chiamante? 2. Le rappresentazioni compilate dei due metodi differiscono?
DavidRR

Risposte:


81

Una delle principali differenze è nella propagazione delle eccezioni. Un'eccezione, generata all'interno di un async Taskmetodo, viene memorizzato nella restituito Taskoggetto e rimane inattivo fino a quando il compito viene osservata tramite await task, task.Wait(), task.Resulto task.GetAwaiter().GetResult(). Viene propagato in questo modo anche se lanciato dalla parte sincrona del asyncmetodo.

Considera il codice seguente, dove OneTestAsynce AnotherTestAsynccomportati in modo abbastanza diverso:

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 chiamo DoTestAsync(OneTestAsync, -2), produce il seguente output:

Premi Invio per continuare
Errore: si sono verificati uno o più errori. Attiva Task.Delay
Errore: 2 °

Nota, ho dovuto premere Enterper vederlo.

Ora, se chiamo DoTestAsync(AnotherTestAsync, -2), il flusso di lavoro del codice all'interno DoTestAsyncè abbastanza diverso, così come l'output. Questa volta non mi è stato chiesto di premere Enter:

Errore: il valore deve essere -1 (che indica un timeout infinito), 0 o un numero intero positivo.
Nome parametro: millisecondsDelayError: 1st

In entrambi i casi Task.Delay(-2)lancia all'inizio, convalidando i suoi parametri. Questo potrebbe essere uno scenario inventato, ma in teoria Task.Delay(1000)potrebbe anche generare, ad esempio, quando l'API del timer di sistema sottostante fallisce.

In una nota a margine, la logica di propagazione dell'errore è ancora diversa per i async voidmetodi (rispetto ai async Taskmetodi). Un'eccezione sollevata all'interno di un async voidmetodo verrà immediatamente rilanciata nel contesto di sincronizzazione del thread corrente (tramite SynchronizationContext.Post), se il thread corrente ne ha uno ( SynchronizationContext.Current != null). Altrimenti, verrà rilanciata tramite ThreadPool.QueueUserWorkItem). Il chiamante non ha la possibilità di gestire questa eccezione sullo stesso stack frame.

Ho pubblicato alcuni dettagli in più sul comportamento di gestione delle eccezioni TPL qui e qui .


D : È possibile imitare il comportamento di propagazione delle eccezioni dei asyncmetodi per metodi non asincroni Task, in modo che questi ultimi non vengano lanciati sullo stesso stack frame?

A : Se davvero necessario, allora sì, c'è un trucco per questo:

// 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;
}

Nota tuttavia, in determinate condizioni (come quando è troppo in profondità nello stack), RunSynchronouslypotrebbe ancora essere eseguito in modo asincrono.


Un'altra differenza notevole è che la versione async/ awaitè più incline al dead-lock su un contesto di sincronizzazione non predefinito . Ad esempio, quanto segue sarà dead-lock in un'applicazione WinForms o WPF:

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

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

Cambialo in una versione non asincrona e non si bloccherà:

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

La natura del dead-lock è ben spiegata da Stephen Cleary nel suo blog .


2
Credo che il deadlock nel primo esempio potrebbe essere evitato aggiungendo .ConfigureAwait (false) alla riga di attesa, poiché accade solo perché il metodo sta cercando di tornare allo stesso contesto di esecuzione. Quindi le eccezioni sono l'unica differenza che rimane.
relativamente_random

2
@relately_random, il tuo commento è corretto, sebbene la risposta riguardasse la differenza tra return Task.Run()e await Task.Run(); return, piuttosto cheawait Task.Run().ConfigureAwait(false); return
noseratio

Se trovi che il programma si chiude dopo aver premuto Invio, assicurati di fare ctrl + F5 invece di F5.
David Klempfner

53

Qual è la differenza tra

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

e

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

?

Sono confuso da questa domanda. Vorrei provare a chiarire rispondendo alla tua domanda con un'altra domanda. Qual è la differenza tra?

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

e

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

?

Qualunque sia la differenza tra le mie due cose, la stessa differenza è tra le tue due cose.


23
Ovviamente! Mi hai aperto gli occhi :) Nel primo caso, creo un'attività wrapper, semanticamente vicina a Task.Delay(1000).ContinueWith(() = {}). Nella seconda, è solo Task.Delay(1000). La differenza è alquanto sottile, ma significativa.
evitare il

3
Potresti spiegare un po 'la differenza? in realtà non lo faccio .. Grazie
zheng yu

4
Data la sottile differenza con i contesti di sincronizzazione e la propagazione delle eccezioni, direi che la differenza tra async / await e wrapper di funzioni non è la stessa.
Cameron MacFarland

1
@CameronMacFarland: Ecco perché ho chiesto chiarimenti. La domanda chiede se esiste una differenza concettuale tra i due. Beh, non lo so. Ci sono certamente molte differenze; qualcuno di loro conta come differenze "concettuali"? Nel mio esempio con le funzioni annidate ci sono anche differenze nella propagazione degli errori; se le funzioni sono chiuse sullo stato locale, ci sono differenze nelle vite locali, e così via. Sono queste differenze "concettuali"?
Eric Lippert

8
Questa è una vecchia risposta, ma credo che data oggi, sarebbe stata sottovalutata. Non risponde alla domanda, né indica all'OP una fonte da cui può imparare.
Daniel Dubovski

11
  1. Il primo metodo non viene nemmeno compilato.

    Poiché " Program.TestAsync()" è un metodo asincrono che restituisce " Task", una parola chiave return non deve essere seguita da un'espressione di oggetto. Avevi intenzione di tornare ' Task<T>'?

    Deve essere

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. C'è una grande differenza concettuale tra questi due. Il primo è asincrono, il secondo no. Leggi Prestazioni asincrone: Comprensione dei costi di Async e Await per ottenere un po 'di più sugli interni di async/ await.

  3. Generano codice diverso.

    .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
    

@ MarcinJuraszek, infatti non è stato compilato. È stato un errore di battitura, sono sicuro che hai capito bene. Altrimenti, un'ottima risposta, grazie! Ho pensato che C # potesse essere abbastanza intelligente da evitare di generare una classe di macchina a stati nel primo caso.
evitare il

9

I due esempi non differiscono. Quando un metodo è contrassegnato con la asyncparola chiave, il compilatore genera una macchina a stati dietro le quinte. Questo è ciò che è responsabile della ripresa delle continuazioni una volta che è stato atteso un atteso.

Al contrario, quando un metodo non è contrassegnato con asyncsi perde la capacità di awaitattesa. (Cioè, all'interno del metodo stesso; il metodo può ancora essere atteso dal suo chiamante.) Tuttavia, evitando la asyncparola chiave, non stai più generando la macchina a stati, che può aggiungere un bel po 'di overhead (sollevando i locali ai campi della macchina a stati, oggetti aggiuntivi al GC).

In esempi come questo, se sei in grado di evitare async-awaite restituire direttamente un atteso, dovrebbe essere fatto per migliorare l'efficienza del metodo.

Vedi questa domanda e questa risposta che sono molto simili alla tua domanda e questa risposta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.