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);
}
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
task = whatTest(n);
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 Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
}
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();
}
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 .
await/asyncw ogóle :)