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 .
awaitasync