Проблема на самом деле является комбинацией вышеперечисленного.Перечисление задач было создано, где каждый раз, когда перечисление повторяется, новый вызов GetDetails
.ToList
вызов этого выбора устранит тупик.Не закрепляя результаты перечислимого (помещая их в список), вызов WhenAll
оценивает перечислимое и асинхронно ожидает итоговые задачи без проблем, но когда вычисляется возвращенный оператор Select, он выполняет итерацию и синхронно ожидает результатызадачи, полученные в результате свежих вызовов GetDetails
и ContinueWith
, которые еще не завершены.Все это синхронное ожидание, вероятно, происходит при попытке сериализовать ответ.
Что касается того, почему это синхронное ожидание вызывает тупик, загадка заключается в том, как ожидают события.Это полностью зависит от того, что вы звоните.Ожидание - это на самом деле просто получение ожидающего с помощью любого видимого в области видимости квалифицирующего метода GetAwaiter
и регистрация обратного вызова, который немедленно вызывает GetResult
для ожидающего, когда работа завершена.Квалификационный метод GetAwaiter
может быть методом экземпляра или расширения, который возвращает объект, имеющий свойство IsCompleted
, метод GetResult
без параметров (любой тип возврата, включая void - результат await), и либо INotifyCompletion
, либо ICriticalNotifyCompletion
интерфейсы.Оба интерфейса имеют OnComplete
методы для регистрации обратного вызова.Есть ошеломляющая цепочка ContinueWith
, и здесь ожидают звонки, и многое из этого зависит от среды выполнения.Поведение по умолчанию для await, получаемого от Task<T>
, заключается в использовании SynchronizationContext.Current
(я думаю, с помощью TaskScheduler.Current
) для вызова обратного вызова или, если это пусто, для использования пула потоков (я думаю, с помощью TaskScheduler.Default
) длявызвать обратный вызов.Метод, содержащий await, оборачивается как Задача некоторым классом CompilerServices
(забыл имя), давая вызывающим методам описанное выше поведение, охватывающее любую ожидаемую реализацию.
A SynchronizationContext
также можетнастроить это, но обычно каждый контекст вызывает в своем собственном отдельном потоке.Если такая реализация присутствует на SynchronizationContext.Current
, когда await
вызывается на Task
, и вы синхронно ожидаете Result
(который сам по себе зависит от вызова ожидающего потока), вы получаете тупик.
С другой стороны, если вы переместили свой метод «как есть» в другой поток, или вызвали ConfigureAwait
для выполнения любой из задач, или скрыли текущий планировщик для ваших вызовов ContinueWith
, или установите вашсобственные SynchronizationContext.Current
(не рекомендуется), вы меняете все вышеперечисленное.