Почему мой асинхронный с ContinueWith тупик? - PullRequest
0 голосов
/ 23 октября 2018

Я здесь не за решением, более подробное объяснение того, что происходит. Я переработал этот код, чтобы предотвратить эту проблему, но я заинтригован, почему этот вызов зашел в тупик.По сути, у меня есть список головных объектов, и мне нужно загрузить каждую из них из объекта хранилища БД (используя Dapper).Я попытался сделать это, используя ContinueWith, но это не удалось:

List<headObj> heads = await _repo.GetHeadObjects();
var detailTasks = heads.Select(s => _changeLogRepo.GetDetails(s.Id)
    .ContinueWith(c => new ChangeLogViewModel() {
         Head = s,
         Details = c.Result
 }, TaskContinuationOptions.OnlyOnRanToCompletion));

await Task.WhenAll(detailTasks);

//deadlock here
return detailTasks.Select(s => s.Result);

Может кто-нибудь объяснить, что вызвало этот тупик? Я попытался осмыслить то, что здесь произошло, но я 'Я не уверен.Я предполагаю, что это как-то связано с вызовом .Result в ContinueWith

Дополнительная информация

  • Это приложение webapi, которое вызывается в контексте async
  • Все звонки репо:

    public async Task<IEnumerable<ItemChangeLog>> GetDetails(int headId)
    {
        using(SqlConnection connection = new SqlConnection(_connectionString))
        {
            return await connection.QueryAsync<ItemChangeLog>(@"SELECT [Id]
             ,[Description]
             ,[HeadId]
                FROM [dbo].[ItemChangeLog]
                WHERE HeadId = @headId", new { headId });
        }
    }
    
  • С тех пор я исправил эту проблему с помощью следующего кода:

     List<headObj> heads = await _repo.GetHeadObjects();
     Dictionary<int, Task<IEnumerable<ItemChangeLog>>> tasks = new Dictionary<int, Task<IEnumerable<ItemChangeLog>>>();
     //get details for each head and build the vm
     foreach(ItemChangeHead head in heads)
     {
           tasks.Add(head.Id, _changeLogRepo.GetDetails(head.Id));
     }
     await Task.WhenAll(tasks.Values);
    
     return heads.Select(s => new ChangeLogViewModel() {
            Head = s,
            Details = tasks[s.Id].Result
        });
    

1 Ответ

0 голосов
/ 24 октября 2018

Проблема на самом деле является комбинацией вышеперечисленного.Перечисление задач было создано, где каждый раз, когда перечисление повторяется, новый вызов 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 (не рекомендуется), вы меняете все вышеперечисленное.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...