Одна велика відмінність полягає у поширенні винятків. Виняток, кинуте всередині 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/ awaitversion є більш схильним до блокування в контексті синхронізації, який не за замовчуванням . Наприклад, у програмі 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взагалі :)