Wszelkie różnice między „await Task.Run (); powrót;" i „return Task.Run ()”?

90

Czy istnieje różnica pojęciowa między następującymi dwoma fragmentami kodu:

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

i

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

Czy wygenerowany kod też się różni?

EDYCJA: Aby uniknąć pomyłki z Task.Runpodobnym przypadkiem:

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

i

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

PÓŹNA AKTUALIZACJA: Oprócz zaakceptowanej odpowiedzi istnieje również różnica w sposobie postępowania LocalCallContext: CallContext.LogicalGetData jest przywracana nawet wtedy, gdy nie ma asynchronii. Czemu?

avo
źródło
1
Tak, to się różni. I bardzo się różni. inaczej nie byłoby sensu używać await/ asyncw ogóle :)
MarcinJuraszek
1
Myślę, że są tutaj dwa pytania. 1. Czy faktyczna implementacja metody ma znaczenie dla wywołującego? 2. Czy skompilowane reprezentacje obu metod różnią się?
DavidRR

Odpowiedzi:

80

Jedna główna różnica dotyczy propagacji wyjątków. Wyjątkiem wyrzucane wewnątrz async Tasksposobie pobiera przechowywane w zwróconym Taskobiektu i pozostaje w uśpieniu aż zadanie zostanie obserwowana przez await task, task.Wait(), task.Resulti task.GetAwaiter().GetResult(). Jest propagowany w ten sposób, nawet jeśli jest wyrzucany z synchronicznej części asyncmetody.

Rozważmy następujący kod, gdzie OneTestAsynci AnotherTestAsynczachowuj się zupełnie inaczej:

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

Jeśli zadzwonię DoTestAsync(OneTestAsync, -2), generuje następujący wynik:

Naciśnij Enter, aby kontynuować
Błąd: wystąpił jeden lub więcej błędów. Czekaj na zadanie.Delay
Błąd: 2

Uwaga, musiałem nacisnąć, Enteraby to zobaczyć.

Teraz, jeśli zadzwonię DoTestAsync(AnotherTestAsync, -2), przepływ pracy kodu wewnątrz DoTestAsyncjest zupełnie inny, podobnie jak dane wyjściowe. Tym razem nie poproszono mnie o naciśnięcie Enter:

Błąd: wartość musi być równa -1 (oznaczająca nieskończony limit czasu), 0 lub dodatnią liczbą całkowitą.
Nazwa parametru: milisekundyDelayError: 1st

W obu przypadkach Task.Delay(-2)wrzuca na początek, podczas walidacji swoich parametrów. Może to być zmyślony scenariusz, ale teoretycznie Task.Delay(1000)może również wystąpić, np. Gdy podstawowy systemowy interfejs API zegara zawiedzie.

Na marginesie, logika propagacji błędów jest jeszcze inna w przypadku async voidmetod (w przeciwieństwie do async Taskmetod). Wyjątek zgłoszony wewnątrz async voidmetody zostanie natychmiast ponownie zgłoszony do kontekstu synchronizacji bieżącego wątku (przez SynchronizationContext.Post), jeśli bieżący wątek go ma ( SynchronizationContext.Current != null). W przeciwnym razie zostanie ponownie wyrzucony za pośrednictwem ThreadPool.QueueUserWorkItem). Wzywający nie ma szans na obsłużenie tego wyjątku w tej samej ramce stosu.

Tutaj i tutaj zamieściłem więcej szczegółów na temat obsługi wyjątków TPL .


P : Czy można naśladować zachowanie propagacji wyjątków asyncmetod dla metod innych niż asynchroniczne Task, aby ta ostatnia nie rzucała się na tę samą ramkę stosu?

O : Jeśli naprawdę potrzebujesz, to tak, jest na to sztuczka:

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

Należy jednak pamiętać, że w pewnych warunkach (np. Gdy jest zbyt głęboko na stosie), RunSynchronouslymoże nadal działać asynchronicznie.


Inną zauważalną różnicą jest to, że / wersja jest bardziej podatna na martwy blokowania na inny niż domyślny kontekście synchronizacji . Na przykład następujące elementy zostaną zablokowane w aplikacji WinForms lub WPF:asyncawait

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

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

Zmień ją na wersję inną niż asynchroniczna i nie będzie się blokować:

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

Naturę zamkniętego zamka dobrze wyjaśnia Stephen Cleary na swoim blogu .

noseratio
źródło
2
Uważam, że zakleszczenie w pierwszym przykładzie można uniknąć, dodając .ConfigureAwait (false) do wiersza await, ponieważ dzieje się tak tylko wtedy, gdy metoda próbuje powrócić do tego samego kontekstu wykonania. Więc wyjątki są jedyną różnicą, która pozostaje.
stosunkowo_random
2
@relatively_random, Twój komentarz jest poprawny, chociaż odpowiedź dotyczyła różnicy między return Task.Run()i await Task.Run(); return, a nieawait Task.Run().ConfigureAwait(false); return
noseratio
Jeśli zauważysz, że program zamyka się po naciśnięciu Enter, upewnij się, że wykonałeś ctrl + F5 zamiast F5.
David Klempfner,
54

Jaka jest różnica pomiędzy

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

i

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

?

Jestem zdezorientowany tym pytaniem. Spróbuję to wyjaśnić, odpowiadając na twoje pytanie innym pytaniem. Jaka jest różnica pomiędzy?

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

i

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

?

Jakakolwiek jest różnica między moimi dwiema rzeczami, ta sama różnica jest między twoimi dwiema rzeczami.

Eric Lippert
źródło
23
Oczywiście! Otworzyłeś mi oczy :) W pierwszym przypadku tworzę zadanie opakowujące, semantycznie bliskie Task.Delay(1000).ContinueWith(() = {}). W drugim jest po prostu Task.Delay(1000). Różnica jest nieco subtelna, ale znacząca.
avo
3
Czy mógłbyś trochę wyjaśnić różnicę? właściwie nie… Dziękuję
zheng yu
4
Biorąc pod uwagę, że istnieje subtelna różnica w kontekstach synchronizacji i propagacji wyjątków, powiedziałbym, że różnica między async / await a opakowaniami funkcji nie jest taka sama.
Cameron MacFarland
1
@CameronMacFarland: Dlatego poprosiłem o wyjaśnienie. Pytanie brzmi, czy istnieje między nimi różnica pojęciowa . No nie wiem. Z pewnością istnieje wiele różnic; czy którekolwiek z nich zalicza się do różnic „koncepcyjnych”? W moim przykładzie z funkcjami zagnieżdżonymi istnieją również różnice w propagacji błędów; jeśli funkcje są zamknięte w stanie lokalnym, istnieją różnice w lokalnych okresach istnienia i tak dalej. Czy są to różnice „pojęciowe”?
Eric Lippert
6
To stara odpowiedź, ale wydaje mi się, że udzielona dzisiaj, zostałaby odrzucona. Nie odpowiada na pytanie ani nie wskazuje PO na źródło, z którego mógłby się dowiedzieć.
Daniel Dubovski
11
  1. Pierwsza metoda nawet się nie kompiluje.

    Ponieważ „ Program.TestAsync()” jest metodą asynchroniczną, która zwraca „ Task”, po słowie kluczowym return nie może następować wyrażenie obiektowe. Czy zamierzałeś wrócić ' Task<T>'?

    To musi być

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Między tymi dwoma istnieje zasadnicza różnica pojęciowa. Pierwsza jest asynchroniczna, druga nie. Przeczytaj artykuł Async Performance: Understanding the Costs of Async and Await, aby uzyskać więcej informacji na temat elementów wewnętrznych async/ await.

  3. Generują inny kod.

    .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
    

    i

    .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
źródło
@MarcinJuraszek, rzeczywiście się nie skompilował. To była literówka, jestem pewien, że masz rację. W przeciwnym razie świetna odpowiedź, dzięki! Pomyślałem, że C # może być wystarczająco inteligentny, aby uniknąć generowania klasy maszyny stanu w pierwszym przypadku.
avo
9

Te dwa przykłady nie różnią się. Gdy metoda jest oznaczonaasync słowem kluczowym, kompilator generuje maszynę stanu za kulisami. To właśnie jest odpowiedzialne za wznowienie kontynuacji, gdy oczekiwano na coś nieoczekiwanego.

W przeciwieństwie do tego, gdy metoda nie jest oznaczona async, tracisz możliwość awaitoczekiwania. (Oznacza to, że w samej metodzie; metoda może nadal być oczekiwana przez obiekt wywołujący). Jednak unikającasync słowa kluczowego, nie generujesz już maszyny stanu, która może dodać sporo narzutu (przenoszenie lokalnych wartości do pól automatu stanowego, dodatkowe obiekty do GC).

W takich przykładach, jeśli możesz tego uniknąć async-await i zwrócić oczekiwaną bezpośrednio, należy to zrobić, aby poprawić skuteczność metody.

Zobacz to pytanie i tę odpowiedź, które są bardzo podobne do twojego pytania i tej odpowiedzi.

Lukazoid
źródło