По запросу в моем веб-API я сохраняю изображение на диск, а также обрабатываю его с помощью внешнего API, что обычно занимает несколько секунд. Это API с высоким трафиком, поэтому я хотел бы разработать его наиболее эффективным способом. Изображение поставляется в "кодировке" Base64, но это не имеет значения. Мы можем думать об этом как о произвольном массиве байтов в среднем 150 КБ (so the saving to disk operation should be really fast).
Рабочий процесс (очевидно, первые две операции не нужно выполнять в любом порядке):
Имея это в виду, я собрал этот (упрощенный) код:
public async Task<IActionResult> Post([FromBody] string imageBase64)
{
// Convert Image
byte[] imageByteArray = Convert.FromBase64String(imageBase64);
// Start async Task to Save Image
Task taskSaveImage = System.IO.File.WriteAllBytesAsync(@"C:\ImagesStorage\image.jpg", imageByteArray);
// Execute some heavy async processing
await ProcessOnExternalAPI(imageByteArray);
// Guarantee that Save Image Task has completed
await taskSaveImage;
// Return 200 Ok
return Ok();
}
Этот код кажется мне самым эффективным способом сохранить образ на диск, а также обработать его внешним API, и то, и другое одновременно, не блокируя при этом рабочий поток ASP.Net CORE. Так ли это, или есть более эффективный способ сделать это?
Кроме того, есть ли проблема с разделением объекта byte[] imageByteArray
между двумя задачами (следовательно, возможно, двумя потоками)? Я считаю, что .Net CORE позаботится об этом, но я не был бы счастлив обнаружить, что я ошибаюсь во время производства.
ProcessOnExternalAPI
выше) выполняется с использованием метода async
PostAsync
из класса System.Net.HttpClient
.Примерно в это же время вы начнете бенчмаркинг.
@deezg наиболее эффективным способом является то, как он выполняет задачи параллельно, сохраняя при этом потоки, которые фактически обрабатывают основные запросы asp.net, разблокированными и доступными для обработки других запросов при выполнении ранее упомянутых задач.
@TheGeneral Итак, на данный момент нет очевидной лучшей реализации? Меня беспокоит, возможно, неспособность увидеть что-то явно неправильное в этом подходе или какое-то предположение, которое я сделал, что на самом деле неверно в этой реализации.
вы не делаете ничего "очевидно неправильного". как сказал @The General, сейчас самое время начать измерять и принимать решение об этом. что я, вероятно, сделал бы, если ваши бизнес-правила позволяют это, так это разгрузить запросы к стороннему API в какую-то очередь. Итак, вы сохраняете свое изображение на диск (или куда угодно) и добавляете ссылку на него в очередь. затем отдельный процесс считывает очередь и отправляет запросы стороннему API. его более сложная инфраструктура, но более эффективная и снимает нагрузку с вашего API.
@deezg Да. Я полностью согласен с тем, что асинхронный подход «запрос-ответ» был бы идеальным, но это невозможно. Это именно моя мотивация поддерживать рабочие потоки как можно более доступными, поскольку мне нужно поддерживать соединение в рабочем состоянии, а клиент продолжает ожидать ответа синхронно, и в то же время есть много других одновременных запросов...
Я думаю, что ваш дисковый ввод-вывод будет дросселироваться раньше всего. Таким образом, я бы рассмотрел возможность использования FileStream и управления размером буфера потока, если это ssd, я бы рассмотрел возможность установки для него максимального размера байта или выше. Также не забывайте об асинхронном флаге, если вы вызываете поток с асинхронными перегрузками. И да, если вы действительно серьезно относитесь к этому, вы, вероятно, захотите использовать шину сообщений и распределить рабочую нагрузку.
@TheGeneral К счастью, это единственная операция с интенсивным дисковым вводом-выводом в моем API, и она полностью соответствует возможностям дисков. Большой трафик на самом деле распределяется между службами этого API, поэтому эта дисковая операция выполняется только для части от общего числа запросов. Но это хорошая мысль (мы никогда не знаем, будет ли следующий вызов), я не думал об этом...
Как я уже сказал, меня больше всего беспокоят потоки, поскольку они нужны мне для других сервисов, которые получают значительно большую долю запросов. Поскольку мой опыт асинхронного программирования в среде веб-запросов не является абсолютно обширным, я хотел бы услышать от сообщества, если я на правильном пути...
Здесь у вас есть только 2 простых вызова. Вы уже поступаете правильно (при условии, что вы не хотите предпринимать более радикальные действия, как обсуждалось). Вы также можете использовать await Task.WhenAll
, хотя заметной разницы во времени выполнения не будет.
Ваш код не самый эффективный, но, вероятно, самый масштабируемый. Вы можете сделать его более эффективным и, вероятно, менее масштабируемым, используя синхронный File.WriteAllBytes
вместо File.WriteAllBytesAsync
. API-интерфейсы асинхронной файловой системы неэффективно реализованы в .NET. В качестве примечания: не ожидая выполнения обеих задач с помощью Task.WhenAll
, вы открываете возможность того, что первая задача станет запущенной и забытой в случае сбоя второй задачи.
Так ли это, или есть более эффективный способ сделать это?
С точки зрения потоков это правильно: ваш код выполняет две параллельные асинхронные операции.
Я предпочитаю использовать Task.WhenAll
, так как это делает цель кода более явной (а также обрабатывает пограничный случай, когда задача записи на диск может превратиться в «выстрелил и забыл», как отметил Теодор в комментариях):
public async Task<IActionResult> Post([FromBody] string imageBase64)
{
// Convert Image
byte[] imageByteArray = Convert.FromBase64String(imageBase64);
// Start async Task to Save Image
Task saveImageTask = System.IO.File.WriteAllBytesAsync(@"C:\ImagesStorage\image.jpg", imageByteArray);
// Start task to call API
Task processTask = ProcessOnExternalAPI(imageByteArray);
// Asynchronously wait for both tasks to complete.
await Task.WhenAll(saveImageTask, processTask);
// Return 200 Ok
return Ok();
}
Кроме того, есть ли проблема с разделением объекта byte[] imageByteArray между двумя задачами (следовательно, возможно, двумя потоками)? Я считаю, что .Net CORE позаботится об этом, но я не был бы счастлив обнаружить, что я ошибаюсь во время производства.
Нет, там нет проблем. Безопасно делиться ценностями, которые не меняются.
Подход Task.WhenAll
действительно делает код более понятным. Что касается проблемы «выстрелил и забыл», это произойдет только в том случае, если я забуду написать ожидание для этой задачи позже (строка await taskSaveImage;
в моем примере кода), верно? Только один момент кажется упущенным: с точки зрения потоков, правильно ли этот подход позволяет рабочему потоку Asp.NET Core быть доступным при выполнении асинхронных операций?
"если забуду" - нет; упомянутая проблема заключается в том, что если ProcessOnExternalAPI
бросает, то taskSaveImage
никогда не await
ed; используя Task.WhenAll
, вы гарантируете, что они оба всегда будут await
ed, даже если один выкинет. Да, этот подход возвращает поток обратно в пул потоков в строке await Task.WhenAll
.
Теперь я понимаю. Спасибо за объяснение!
каковы ваши критерии "наиболее эффективного способа"? вы измеряете его по количеству секунд, которое требуется для завершения этого метода API? или количество ресурсов, которые вы используете? или как-то иначе?