Jeder Unterschied zwischen „warte auf Task.Run (); Rückkehr;" und "Task.Run () zurückgeben"?

90

Gibt es einen konzeptionellen Unterschied zwischen den folgenden zwei Codeteilen:

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

und

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

Unterscheidet sich auch der generierte Code?

EDIT: Um Verwechslungen mit Task.Runeinem ähnlichen Fall zu vermeiden :

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

und

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

LATE UPDATE: Zusätzlich zur akzeptierten Antwort gibt es auch einen Unterschied in der BehandlungLocalCallContext : CallContext.LogicalGetData wird auch dann wiederhergestellt, wenn keine Asynchronität vorliegt. Warum?

avo
quelle
1
Ja, das ist anders. Und es ist sehr unterschiedlich. sonst wäre es awaitasync
sinnlos
1
Ich denke, hier gibt es zwei Fragen. 1. Ist die tatsächliche Implementierung der Methode für den Aufrufer von Bedeutung? 2. Unterscheiden sich die kompilierten Darstellungen der beiden Methoden?
DavidRR

Antworten:

80

Ein Hauptunterschied besteht in der Weitergabe von Ausnahmen. Eine Ausnahme, innerhalb einer geworfene async TaskMethode wird in dem zurück gespeicherte TaskObjekt und bleibt inaktiv , bis die Aufgabe , über beobachtet wird await task, task.Wait(), task.Resultoder task.GetAwaiter().GetResult(). Es wird auf diese Weise weitergegeben, selbst wenn es aus dem synchronen Teil der asyncMethode geworfen wird.

Betrachten Sie den folgenden Code ein , wo OneTestAsyncund AnotherTestAsyncverhalten sich ganz anders:

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

Wenn ich anrufe DoTestAsync(OneTestAsync, -2), wird die folgende Ausgabe erzeugt:

Drücken Sie die Eingabetaste, um fortzufahren
Fehler: Ein oder mehrere Fehler sind aufgetreten.await Task.Delay
Fehler: 2 ..

Beachten Sie, ich musste drücken Enter, um es zu sehen.

Wenn ich jetzt anrufe DoTestAsync(AnotherTestAsync, -2), ist der Code-Workflow im Inneren DoTestAsyncganz anders, ebenso wie die Ausgabe. Dieses Mal wurde ich nicht gebeten zu drücken Enter:

Fehler: Der Wert muss entweder -1 (was eine unendliche Zeitüberschreitung bedeutet), 0 oder eine positive Ganzzahl sein.
Parametername: MillisekundenDelayError: 1st

In beiden Fällen Task.Delay(-2) am Anfang geworfen, während die Parameter überprüft werden. Dies kann ein erfundenes Szenario sein, kann aber theoretisch Task.Delay(1000)auch auslösen, z. B. wenn die zugrunde liegende Systemzeitgeber-API ausfällt.

Nebenbei bemerkt unterscheidet sich die Fehlerausbreitungslogik für async voidMethoden (im Gegensatz zu async TaskMethoden). Eine innerhalb einer async voidMethode ausgelöste Ausnahme wird sofort im Synchronisationskontext des aktuellen Threads (via SynchronizationContext.Post) erneut ausgelöst , sofern der aktuelle Thread eine hat (SynchronizationContext.Current != null) andernfalls wird sie via erneut ausgelöst ThreadPool.QueueUserWorkItem). Der Aufrufer hat keine Chance, diese Ausnahme auf demselben Stapelrahmen zu behandeln.

Ich habe hier und hier einige weitere Details zum Verhalten bei der Behandlung von TPL-Ausnahmen veröffentlicht .


F : Ist es möglich, das Ausnahmeverbreitungsverhalten von asyncMethoden für nicht asynchrone TaskMethoden nachzuahmen , damit diese nicht auf denselben Stapelrahmen werfen?

A : Wenn es wirklich gebraucht wird, gibt es dafür einen Trick:

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

Beachten Sie jedoch, dass die Ausführung unter bestimmten Bedingungen (z. B. wenn sie zu tief im Stapel liegt) RunSynchronouslyweiterhin asynchron ausgeführt werden kann.


Ein weiterer bemerkenswerter Unterschied besteht darin, dass die Version async/ awaitanfälliger für Deadlocks in einem nicht standardmäßigen Synchronisationskontext ist . In einer WinForms- oder WPF-Anwendung wird beispielsweise Folgendes blockiert:

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

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

Ändern Sie es in eine nicht asynchrone Version und es wird nicht blockiert:

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

Die Art der Sackgasse wird von Stephen Cleary in seinem Blog gut erklärt .

noseratio
quelle
2
Ich glaube, der Deadlock im ersten Beispiel könnte vermieden werden, indem .ConfigureAwait (false) zur Wartezeile hinzugefügt wird, da dies nur geschieht, weil die Methode versucht, zum gleichen Ausführungskontext zurückzukehren. Dann sind die Ausnahmen der einzige verbleibende Unterschied.
relativ
2
@relativ_random, Ihr Kommentar ist korrekt, obwohl die Antwort über den Unterschied zwischen return Task.Run()und await Task.Run(); returnstattawait Task.Run().ConfigureAwait(false); return
noseratio
Wenn Sie feststellen, dass das Programm geschlossen wird, nachdem Sie die Eingabetaste gedrückt haben, stellen Sie sicher, dass Sie Strg + F5 anstelle von F5 drücken.
David Klempfner
56

Was ist der Unterschied zwischen

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

und

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

?

Diese Frage verwirrt mich. Lassen Sie mich versuchen, dies zu klären, indem Sie Ihre Frage mit einer anderen Frage beantworten. Was ist der Unterschied zwischen?

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

und

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

?

Was auch immer der Unterschied zwischen meinen beiden Dingen ist, der gleiche Unterschied besteht zwischen Ihren beiden Dingen.

Eric Lippert
quelle
21
Natürlich! Du hast meine Augen geöffnet :) Im ersten Fall erstelle ich eine Wrapper-Aufgabe, die semantisch nahe ist Task.Delay(1000).ContinueWith(() = {}). Im zweiten ist es einfach Task.Delay(1000). Der Unterschied ist etwas subtil, aber signifikant.
Avo
2
Könnten Sie den Unterschied etwas erklären? Eigentlich weiß ich nicht
zheng yu
4
Angesichts der Tatsache, dass es einen subtilen Unterschied zu Synchronisierungskontexten gibt und die Ausnahmeverbreitung würde ich sagen, dass der Unterschied zwischen Async / Await- und Funktions-Wrappern nicht der gleiche ist.
Cameron MacFarland
1
@CameronMacFarland: Deshalb habe ich um Klarstellung gebeten. Die Frage ist, ob es einen konzeptionellen Unterschied zwischen den beiden gibt. Nun, ich weiß es nicht. Es gibt sicherlich viele Unterschiede; Zählt einer von ihnen als "konzeptionelle" Unterschiede? In meinem Beispiel mit verschachtelten Funktionen gibt es auch Unterschiede in der Fehlerausbreitung. Wenn die Funktionen über dem lokalen Zustand geschlossen sind, gibt es Unterschiede in der lokalen Lebensdauer und so weiter. Sind das "konzeptionelle" Unterschiede?
Eric Lippert
5
Dies ist eine alte Antwort, aber ich glaube, dass sie heute abgelehnt worden wäre. Es beantwortet weder die Frage noch verweist es das OP auf eine Quelle, aus der er lernen kann.
Daniel Dubovski
11
  1. Die erste Methode kompiliert nicht einmal.

    Da ' Program.TestAsync()' eine asynchrone Methode ist, die ' Task' zurückgibt, darf auf ein return-Schlüsselwort kein Objektausdruck folgen. Wollten Sie zurückkehren Task<T>?

    Es muss sein

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
  2. Es gibt einen großen konzeptionellen Unterschied zwischen diesen beiden. Der erste ist asynchron, der zweite nicht. Lesen Sie Async Performance: Die Kosten von Async verstehen und warten , um mehr über die Interna von async/ zu erfahrenawait .

  3. Sie generieren unterschiedlichen Code.

    .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

    und

    .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
quelle
@MarcinJuraszek, tatsächlich wurde es nicht kompiliert. Das war ein Tippfehler, ich bin sicher, Sie haben es richtig verstanden. Ansonsten eine tolle Antwort, danke! Ich dachte, C # könnte klug genug sein, um im ersten Fall das Generieren einer Zustandsmaschinenklasse zu vermeiden.
Avo
9

Die beiden Beispiele haben unterscheiden. Wenn eine Methode mit dem gekennzeichnet istasync Schlüsselwort , generiert der Compiler hinter den Kulissen eine Zustandsmaschine. Dies ist verantwortlich für die Wiederaufnahme der Fortsetzung, sobald ein Warten erwartet wurde.

Wenn im Gegensatz dazu eine Methode nicht mit markiert asyncist, verlieren Sie die Fähigkeit, zu awaitwarten. (Das heißt, innerhalb der Methode selbst; die Methode kann immer noch von ihrem Aufrufer erwartet werden.) Wenn Sie jedoch das asyncSchlüsselwort vermeiden , generieren Sie nicht mehr die Zustandsmaschine, die einiges an Overhead hinzufügen kann (das Heben von Einheimischen zu Feldern) der Zustandsmaschine, zusätzliche Objekte zum GC).

Wenn Sie in Beispielen in der Lage sind, async-awaitein Erwartetes direkt zu vermeiden und zurückzugeben, sollte dies getan werden, um die Effizienz der Methode zu verbessern.

Sehen Sie sich diese Frage und diese Antwort an, die Ihrer Frage und dieser Antwort sehr ähnlich sind.

Lukazoid
quelle