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