У меня есть следующий код, который я собираюсь запускать асинхронно. Моя цель состоит в том, чтобы 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() не ожидается. Я прав в этом? Если нет, то как мне изменить структуру кода, чтобы добиться того, чего я хочу?
Все сказанное, в конечном счете, не совсем понятно, о чем вы спрашиваете. Распараллеливание отлично работает с async/await, если сделано правильно, и плохо, когда нет. Как и все остальное. Async/await на самом деле не относится к распараллеливанию как таковому. Его можно использовать в таких контекстах, но на самом деле это более общая концепция для использования с асинхронным завершением работы Любые, независимо от того, включает ли это параллельную обработку некоторой совокупности работ или одиночной асинхронной операции.
Это похоже на случай TPL DataFlow или RX.
Я предлагаю вам следовать соглашению о добавлении суффикса к именам методов, возвращающих задачу, с помощью Async.
Если вам нужно ограничить параллельное выполнение, не прибегая к экзотическим решениям (TPL DataFlow, Reactive Extensions), вам могут пригодиться эти две ссылки: Подходы к регулированию асинхронных методов в C#, Реализация простого ForEachAsync, часть 2.
@PeterDuniho, я спрашивал, что именно вы сказали в первом комментарии. Мне было понятно, что это так, но я не был на 100% уверен в работе WhenAll. Не могли бы вы опубликовать это как ответ, чтобы я мог принять его?





При вызове асинхронного метода следует помнить, что как только внутри этого метода достигается оператор 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. В частности, в то время как исходный код выполнял поиск изображения и документа одновременно, ваша версия сериализует эти операции, что может привести к снижению производительности и увеличению задержки с точки зрения пользователя.
@PeterDuniho, насколько я вижу, исходный код также сериализует две операции (из-за двух await внутри CreatePicture).
Вы забыли async в подписи GetPictureForEmployeeAsync. Кроме того, что касается ограниченное количество выделенных потоков, мы понятия не имеем о внутренней работе асинхронных методов GetBase64PictureAsync и GetItemAsync. Я предполагаю, что, вероятно, нет нити.
@Байрон: "исходный код сериализует две операции" -- нет. await не сериализуют операции, потому что вторая задача уже запущена до того, как будет достигнута первая await. В вашем коде вторая задача не будет даже Начало, пока не завершится первая await. Это совсем другое.
Пара слов @PeterDuniho: Что касается "исходный код сериализует две операции" — я этого не говорил. Также имейте в виду, что ОП стремился лучше понять, как работают его асинхронные методы, которые я предоставил. ОП может использовать эти принципы для решения своей проблемы так, как считает нужным. Кроме того, вы, можно иметь, правильно указали, как можно улучшить мой пример, но, поскольку никто из нас не знает, какие 2 важных метода здесь делают под капотом, мы не можем сказать наверняка. Это не ответ типа «просто вставьте мой код», как указывает последняя часть моего ответа.
Байрон: "Я этого не говорил" -- извините, вы правы. Мой комментарий должен был быть адресован @Theodor. Тем не менее, изменение семантики между кодом OP и вашим ответом - это ИМХО то, что, по крайней мере, должно быть указано явно, с объяснением того, почему вы считаете, что это изменение оправдано, и, вероятно, его следует избегать.
@PeterDuniho, ты прав, моя ошибка. Легко ошибиться, когда у вас мало кофе. ?
В этом ответе есть ряд ошибок, в основном мелких. Я не хочу быть придирчивым, но важно объяснить истины, пытаясь объяснить, как это работает людям, которые этого не понимают, потому что есть тонкости, которые иногда нужно ценить.
Например, «как только внутри этого метода достигается оператор await, управление немедленно возвращается к коду, вызвавшему асинхронный метод». Нет, это не так. Если задача имеет уже закончил нормально, то результат извлекается, и сопрограмма не дает результата. Если у задачи есть уже завершено ненормально, то выхода нет; скорее вызывается механизм обработки исключений. Только если задача не завершена, текущий рабочий процесс уступает вызывающей стороне.
Например, «с обычным методом управление не возвращается к коду, который вызывает этот метод, пока не будет достигнут конец этого метода». Я понимаю, к чему ты клонишь, но будь точен. В обычном методе, то есть не в асинхронном методе или блоке итератора, управление обычно возвращается вызывающей стороне, когда достигнут конец метода или конец оператора return, а управление возвращается ненормально, когда конец чего-то, что вызывает исключение, не перехваченное методом, достигнут, если только не будет finally... и так далее.
«Это приведет к тому, что все элементы сотрудников будут обрабатываться параллельно», не должно быть никакого параллелизма потоков, если задача связана с вводом-выводом. Асинхронные задачи, связанные с вводом-выводом, выполняются на уровне ниже потоков ОС; любой параллелизм в них является результатом аппаратного обеспечения низкого уровня, а не высокоуровневых структур операционной системы, таких как потоки. Я бы не рекомендовал пытаться использовать настройки параллелизма для управления шлюзованием соединений, так как все соединения могут происходить в одном и том же потоке!
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: Вы могли бы. Я предпочитаю использовать await, потому что он более защищен от рефакторинга. Если метод изменяется так, что выполнение задач больше не гарантируется (с наблюдаемыми исключениями), то await ведет себя правильно.
WhenAll(), в конечном итоге завершится, когда каждая из отдельных ожидаемых задач будет завершена. Реализация, которую вы имеете, кажется разумной (несмотря на ошибки времени компиляции); методGetPictureForEmployee()возвращает задачу, возвращеннуюCreatePicture(), вы создаете несколько таких задач благодаря тому, чтоSelect()проецирует ваши входные данные на эти задачи, а затем асинхронно ждете завершения всех таких задач.