Как работает распараллеливание в async/await?

У меня есть следующий код, который я собираюсь запускать асинхронно. Моя цель состоит в том, чтобы GetPictureForEmployeeAsync() вызывался параллельно столько раз, сколько необходимо. Я хотел бы убедиться, что «ожидание» в CreatePicture не мешает мне это сделать.

    public Task<Picture[]> GetPictures(IDictionary<string, string> tags)
    {
        var query = documentRepository.GetRepositoryQuery();

        var employees = query.Where(doc => doc.Gender == tags["gender"]);

        return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));
    }

    private Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
    {
        var base64PictureTask = blobRepository.GetBase64PictureAsync(employee.ID.ToString());
        var documentTask = documentRepository.GetItemAsync(employee.ID.ToString());
        return CreatePicture(tags, base64PictureTask, documentTask);
    }

    private static async Task<Picture> CreatePicture(IDictionary<string, string> tags, Task<string> base64PictureTask, Task<Employee> documentTask)
    {
        var document = await documentTask;

        return new Picture
        {
            EmployeeID = document.ID,
            Data = await base64PictureTask,
            ID = document.ID.ToString(),
            Tags = tags,
        };
    }

Если я правильно понимаю, на Task.WhenAll() не влияют две ожидаемые задачи внутри CreatePicture(), потому что GetPictureForEmployeeAsync() не ожидается. Я прав в этом? Если нет, то как мне изменить структуру кода, чтобы добиться того, чего я хочу?

"Task.WhenAll не затрагивается" -- как повлияло? Это, безусловно, влияет в том смысле, что каждая задача, переданная WhenAll(), в конечном итоге завершится, когда каждая из отдельных ожидаемых задач будет завершена. Реализация, которую вы имеете, кажется разумной (несмотря на ошибки времени компиляции); метод GetPictureForEmployee() возвращает задачу, возвращенную CreatePicture(), вы создаете несколько таких задач благодаря тому, что Select() проецирует ваши входные данные на эти задачи, а затем асинхронно ждете завершения всех таких задач.
Peter Duniho 30.06.2019 23:44

Все сказанное, в конечном счете, не совсем понятно, о чем вы спрашиваете. Распараллеливание отлично работает с async/await, если сделано правильно, и плохо, когда нет. Как и все остальное. Async/await на самом деле не относится к распараллеливанию как таковому. Его можно использовать в таких контекстах, но на самом деле это более общая концепция для использования с асинхронным завершением работы Любые, независимо от того, включает ли это параллельную обработку некоторой совокупности работ или одиночной асинхронной операции.

Peter Duniho 30.06.2019 23:46

Это похоже на случай TPL DataFlow или RX.

TheGeneral 01.07.2019 00:41

Я предлагаю вам следовать соглашению о добавлении суффикса к именам методов, возвращающих задачу, с помощью Async.

Theodor Zoulias 01.07.2019 03:05

Если вам нужно ограничить параллельное выполнение, не прибегая к экзотическим решениям (TPL DataFlow, Reactive Extensions), вам могут пригодиться эти две ссылки: Подходы к регулированию асинхронных методов в C#, Реализация простого ForEachAsync, часть 2.

Theodor Zoulias 01.07.2019 03:13

@PeterDuniho, я спрашивал, что именно вы сказали в первом комментарии. Мне было понятно, что это так, но я не был на 100% уверен в работе WhenAll. Не могли бы вы опубликовать это как ответ, чтобы я мог принять его?

Zalomon 01.07.2019 12:20
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
185
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

При вызове асинхронного метода следует помнить, что как только внутри этого метода достигается оператор await, элемент управления немедленно возвращается к коду, вызвавшему асинхронный метод, независимо от того, где находится оператор await. метод. С «нормальным» методом управление не возвращается к коду, который вызывает этот метод, пока не будет достигнут конец этого метода.

Итак, в вашем случае вы можете сделать следующее:

private async Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
    {
        // As soon as we get here, control immediately goes back to the GetPictures
        //   method -- no need to store the task in a variable and await it within
        //   CreatePicture as you were doing
        var picture = await blobRepository.GetBase64PictureAsync(employee.ID.ToString());
        var document = await documentRepository.GetItemAsync(employee.ID.ToString());
        return CreatePicture(tags, picture, document);
    }

Поскольку первая строка кода в GetPictureForEmployeeAsync имеет await, управление немедленно вернется к этой строке...

return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));

... как только он будет вызван. Это приведет к тому, что все элементы данных о сотрудниках будут обрабатываться параллельно (ну, вроде того, что количество потоков, выделенных вашему приложению, будет ограничено).

В качестве дополнительного совета: если это приложение обращается к базе данных или веб-службе для получения изображений или документов, этот код, скорее всего, вызовет у вас проблемы с исчерпанием доступных подключений. В этом случае рассмотрите возможность использования System.Threading.Tasks.Parallel и установки максимальной степени параллелизма или используйте SemaphoreSlim для управления количеством соединений, используемых одновременно.

Обратите внимание, что предложенная вами альтернатива значительно и, откровенно говоря, негативно изменила семантику кода OP. В частности, в то время как исходный код выполнял поиск изображения и документа одновременно, ваша версия сериализует эти операции, что может привести к снижению производительности и увеличению задержки с точки зрения пользователя.

Peter Duniho 01.07.2019 00:12

@PeterDuniho, насколько я вижу, исходный код также сериализует две операции (из-за двух await внутри CreatePicture).

Theodor Zoulias 01.07.2019 02:57

Вы забыли async в подписи GetPictureForEmployeeAsync. Кроме того, что касается ограниченное количество выделенных потоков, мы понятия не имеем о внутренней работе асинхронных методов GetBase64PictureAsync и GetItemAsync. Я предполагаю, что, вероятно, нет нити.

Theodor Zoulias 01.07.2019 03:02

@Байрон: "исходный код сериализует две операции" -- нет. await не сериализуют операции, потому что вторая задача уже запущена до того, как будет достигнута первая await. В вашем коде вторая задача не будет даже Начало, пока не завершится первая await. Это совсем другое.

Peter Duniho 01.07.2019 05:24

Пара слов @PeterDuniho: Что касается "исходный код сериализует две операции" — я этого не говорил. Также имейте в виду, что ОП стремился лучше понять, как работают его асинхронные методы, которые я предоставил. ОП может использовать эти принципы для решения своей проблемы так, как считает нужным. Кроме того, вы, можно иметь, правильно указали, как можно улучшить мой пример, но, поскольку никто из нас не знает, какие 2 важных метода здесь делают под капотом, мы не можем сказать наверняка. Это не ответ типа «просто вставьте мой код», как указывает последняя часть моего ответа.

Byron Jones 01.07.2019 15:11

Байрон: "Я этого не говорил" -- извините, вы правы. Мой комментарий должен был быть адресован @Theodor. Тем не менее, изменение семантики между кодом OP и вашим ответом - это ИМХО то, что, по крайней мере, должно быть указано явно, с объяснением того, почему вы считаете, что это изменение оправдано, и, вероятно, его следует избегать.

Peter Duniho 01.07.2019 18:29

@PeterDuniho, ты прав, моя ошибка. Легко ошибиться, когда у вас мало кофе. ?

Theodor Zoulias 01.07.2019 18:44

В этом ответе есть ряд ошибок, в основном мелких. Я не хочу быть придирчивым, но важно объяснить истины, пытаясь объяснить, как это работает людям, которые этого не понимают, потому что есть тонкости, которые иногда нужно ценить.

Eric Lippert 01.07.2019 20:19

Например, «как только внутри этого метода достигается оператор await, управление немедленно возвращается к коду, вызвавшему асинхронный метод». Нет, это не так. Если задача имеет уже закончил нормально, то результат извлекается, и сопрограмма не дает результата. Если у задачи есть уже завершено ненормально, то выхода нет; скорее вызывается механизм обработки исключений. Только если задача не завершена, текущий рабочий процесс уступает вызывающей стороне.

Eric Lippert 01.07.2019 20:20

Например, «с обычным методом управление не возвращается к коду, который вызывает этот метод, пока не будет достигнут конец этого метода». Я понимаю, к чему ты клонишь, но будь точен. В обычном методе, то есть не в асинхронном методе или блоке итератора, управление обычно возвращается вызывающей стороне, когда достигнут конец метода или конец оператора return, а управление возвращается ненормально, когда конец чего-то, что вызывает исключение, не перехваченное методом, достигнут, если только не будет finally... и так далее.

Eric Lippert 01.07.2019 20:23

«Это приведет к тому, что все элементы сотрудников будут обрабатываться параллельно», не должно быть никакого параллелизма потоков, если задача связана с вводом-выводом. Асинхронные задачи, связанные с вводом-выводом, выполняются на уровне ниже потоков ОС; любой параллелизм в них является результатом аппаратного обеспечения низкого уровня, а не высокоуровневых структур операционной системы, таких как потоки. Я бы не рекомендовал пытаться использовать настройки параллелизма для управления шлюзованием соединений, так как все соединения могут происходить в одном и том же потоке!

Eric Lippert 01.07.2019 20:24
Ответ принят как подходящий

I'd like to make sure that 'await' on CreatePicture does not prevent me from doing so.

Это не так.

If I understand it correctly, Task.WhenAll() is not affected by the two awaited tasks inside CreatePicture() because GetPictureForEmployeeAsync() is not awaited. Am I right about this?

И да и нет. WhenAll никоим образом не ограничен ожидаемыми задачами в CreatePicture, но это не имеет никакого отношения к тому, ожидается ли GetPictureForEmployeeAsync или нет. Эти две строки кода эквивалентны с точки зрения поведения:

return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));
return Task.WhenAll(employees.Select(async employee => await GetPictureForEmployeeAsync(employee, tags)));

Я рекомендую прочитать мой асинхронное введение, чтобы получить хорошее представление о том, как async и await работают с задачами.

Кроме того, поскольку GetPictures имеет нетривиальную логику (GetRepositoryQuery и вычисление tags["gender"]), я рекомендуем использовать async и await для GetPictures как таковой:

public async Task<Picture[]> GetPictures(IDictionary<string, string> tags)
{
  var query = documentRepository.GetRepositoryQuery();
  var employees = query.Where(doc => doc.Gender == tags["gender"]);
  var tasks = employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)).ToList();

  return await Task.WhenAll(tasks);
}

И последнее замечание: вы можете найти свой код чище, если не пропустите «задачи, которые должны быть ожидаемы» — вместо этого await их сначала и передайте их значения результатов:

async Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
{
  var base64PictureTask = blobRepository.GetBase64PictureAsync(employee.ID.ToString());
  var documentTask = documentRepository.GetItemAsync(employee.ID.ToString());
  await Task.WhenAll(base64PictureTask, documentTask);
  return CreatePicture(tags, await base64PictureTask, await documentTask);
}

static Picture CreatePicture(IDictionary<string, string> tags, string base64Picture, Employee document)
{
  return new Picture
  {
    EmployeeID = document.ID,
    Data = base64Picture,
    ID = document.ID.ToString(),
    Tags = tags,
  };
}

Спасибо за отличный ответ, я хотел бы задать еще вопрос. При вызове CreatePicture() вы ожидаете задачи, которые были созданы ранее и которые ожидаются в предыдущей строке. Не было бы безопасно использовать результат в этом случае? Так как мы знаем, что обе задачи выполнены.

Zalomon 02.07.2019 08:14

@Zalomon: Вы могли бы. Я предпочитаю использовать await, потому что он более защищен от рефакторинга. Если метод изменяется так, что выполнение задач больше не гарантируется (с наблюдаемыми исключениями), то await ведет себя правильно.

Stephen Cleary 02.07.2019 13:50

Другие вопросы по теме