Проблема в том, что вы используете неуниверсальный Task
класс, который не предназначен для получения результата. Поэтому, когда вы создаете Task
экземпляр, передающий асинхронный делегат:
Task myTask = new Task(async () =>
... делегат рассматривается как async void
. An async void
не является Task
, его нельзя ожидать, его исключение не может быть обработано, и это источник тысяч вопросов, задаваемых разочарованными программистами здесь, в StackOverflow и в других местах. Решение состоит в том, чтобы использовать универсальный Task<TResult>
класс, потому что вы хотите вернуть результат, а результат - другой Task
. Итак, вы должны создать Task<Task>
:
Task<Task> myTask = new Task<Task>(async () =>
Теперь, когда вы Start
внешнее, Task<Task>
оно будет завершено почти мгновенно, потому что его задача - просто создать внутреннее Task
. Затем вам придется ждать и внутреннего Task
. Вот как это можно сделать:
myTask.Start();
Task myInnerTask = await myTask;
await myInnerTask;
У вас есть две альтернативы. Если вам не нужна явная ссылка на внутреннее, Task
тогда вы можете просто ждать внешнего Task<Task>
дважды:
await await myTask;
... или вы можете использовать встроенный метод расширения, Unwrap
который объединяет внешние и внутренние задачи в одну:
await myTask.Unwrap();
Это развертывание происходит автоматически, когда вы используете гораздо более популярный Task.Run
метод, который создает горячие задачи, поэтому в Unwrap
настоящее время он используется не очень часто.
Если вы решите, что ваш асинхронный делегат должен вернуть результат, например, a string
, то вы должны объявить myTask
переменную типа Task<Task<string>>
.
Примечание: я не поддерживаю использование Task
конструкторов для создания холодных задач. Как правило, практика не одобряется по причинам, которые я на самом деле не знаю, но, вероятно, потому, что она используется настолько редко, что потенциально может застать врасплох других неосведомленных пользователей / сопровождающих / рецензентов кода.
Общий совет: будьте осторожны каждый раз, когда вы предоставляете асинхронный делегат в качестве аргумента метода. Этот метод в идеале должен ожидать Func<Task>
аргумент (что означает, что он понимает асинхронные делегаты) или, по крайней мере, Func<T>
аргумент (то есть, что, по крайней мере, сгенерированный Task
не будет проигнорирован). В неудачном случае, когда этот метод принимает Action
, ваш делегат будет рассматриваться как async void
. Это редко то, что вы хотите, если когда-либо.