Проблема в том, что вы используете неуниверсальный 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. Это редко то, что вы хотите, если когда-либо.