Одно из основных различий заключается в распространении исключений. Исключение, брошенное внутри async Taskметоды, сохраняется в возвращенном Taskобъекте и остается бездействующим , пока задача не получает наблюдаются через await task, task.Wait(), task.Resultили task.GetAwaiter().GetResult(). Таким образом он распространяется, даже если его выбрасывают из синхронной части asyncметода.
Рассмотрим следующий код, где OneTestAsyncи AnotherTestAsyncведут себя совершенно иначе:
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);
}
}
Если я позвоню DoTestAsync(OneTestAsync, -2), он выдаст следующий результат:
Нажмите Enter, чтобы продолжить
Ошибка: произошла одна или несколько ошибок. Ожидайте Task.Delay
Ошибка: 2-я
Обратите внимание, мне пришлось нажать, Enterчтобы увидеть это.
Теперь, если я позвоню DoTestAsync(AnotherTestAsync, -2), рабочий процесс кода внутри DoTestAsyncбудет совсем другим, как и результат. На этот раз меня не просили нажимать Enter:
Ошибка: значение должно быть либо -1 (означает бесконечный тайм-аут), либо 0, либо положительным целым числом.
Имя параметра: millisecondsDelayError: 1st
В обоих случаях Task.Delay(-2)бросает в начале, проверяя его параметры. Это может быть выдуманный сценарий, но теоретически он также Task.Delay(1000)может сработать, например, при отказе базового API системного таймера.
Кстати, логика распространения ошибок отличается для async voidметодов (в отличие от async Taskметодов). Исключение, возникшее внутри async voidметода, будет немедленно повторно выбрано в контексте синхронизации текущего потока (через SynchronizationContext.Post), если у текущего потока он есть ( SynchronizationContext.Current != null). В противном случае оно будет повторно выбрано через ThreadPool.QueueUserWorkItem). У вызывающей стороны нет возможности обработать это исключение в том же кадре стека.
Я разместил более подробную информацию о поведении обработки исключений TPL здесь и здесь .
В : Можно ли имитировать поведение распространения исключений для asyncметодов, не Taskоснованных на асинхронном режиме , чтобы последние не создавали один и тот же кадр стека?
О : Если действительно нужно, то да, для этого есть трюк:
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;
}
Однако обратите внимание, что при определенных условиях (например, когда он находится слишком глубоко в стеке) RunSynchronouslyвсе еще может выполняться асинхронно.
Еще одно заметное отличие заключается в том,
что async/ awaitверсия более подвержена мертвой блокировке в контексте синхронизации, отличном
от используемого по умолчанию . Например, в приложении WinForms или WPF будет заблокировано следующее:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
Измените его на неасинхронную версию, и он не будет блокироваться:
Task TestAsync()
{
return Task.Delay(1000);
}
Природу тупика хорошо объяснил Стивен Клири в своем блоге .
await/asyncвообще :)