TL; DR: async
работает с ожидающими, а не с заданиями.Из-за этого в конце метода требуется дополнительный бит логики для перевода статуса ожидающего в задачу.
Ваше предположение, что продолжения нет, неверно.Было бы верно, если бы вы только что вернули задачу:
Task DelayAsync()
{
return Task.Delay(3000);
}
Однако все становится сложнее, когда вы помечаете метод как async
.Одним важным свойством метода async
является способ обработки исключений.Рассмотрим эти методы, например:
Task NoAsync()
{
throw new Exception();
}
async Task Async()
{
throw new Exception();
}
Что теперь произойдет, если вы вызовете их?
var task1 = NoAsync(); // Throws an exception
var task2 = Async(); // Returns a faulted task
Разница в том, что асинхронная версия переносит исключение в возвращаемую задачу.
Какое это имеет отношение к нашему случаю?
Когда вы await
метод, компилятор фактически вызывает GetAwaiter()
для объекта, который вы ожидаете.Ожидающий определяет 3 члена:
- Свойство
IsCompleted
- Метод
OnCompleted
- Метод
GetResult
Как вы можете видеть, нет члена, возвращающего исключение.Как узнать, виноват ли виновник?Чтобы знать это, вам нужно вызвать метод GetResult
, который вызовет исключение.
Вернемся к вашему примеру:
async Task DelayAsync()
{
await Task.Delay(3000);
}
Если Task.Delay
выдает исключение, async
машина должна установить статус возвращенной задачи как неисправной.Чтобы узнать, выдало ли Task.Delay
исключение, необходимо вызвать GetResult
на приемнике после завершения Task.Delay
.Следовательно, у вас есть продолжение, хотя это не очевидно при просмотре кода.Под капотом асинхронный метод выглядит следующим образом:
Task DelayAsync()
{
var tcs = new TaskCompletionSource<object>();
try
{
var awaiter = Task.Delay(3000).GetAwaiter();
awaiter.OnCompleted(() =>
{
// This is the continuation that causes your deadlock
try
{
awaiter.GetResult();
tcs.SetResult(null);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
Фактический код более сложен и использует AsyncTaskMethodBuilder<T>
вместо TaskCompletionSource<T>
, но идея та же.