Самый эффективный способ сохранить файл на диске при выполнении асинхронной работы в веб-запросе ASP.Net CORE

По запросу в моем веб-API я сохраняю изображение на диск, а также обрабатываю его с помощью внешнего API, что обычно занимает несколько секунд. Это API с высоким трафиком, поэтому я хотел бы разработать его наиболее эффективным способом. Изображение поставляется в "кодировке" Base64, но это не имеет значения. Мы можем думать об этом как о произвольном массиве байтов в среднем 150 КБ (so the saving to disk operation should be really fast).

Рабочий процесс (очевидно, первые две операции не нужно выполнять в любом порядке):

  • Сохранить изображение на диск (асинхронно)
  • Обработайте его на внешнем API (асинхронно)
  • Вернуть Ok в случае успешного завершения обеих предыдущих операций

Имея это в виду, я собрал этот (упрощенный) код:

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 позаботится об этом, но я не был бы счастлив обнаружить, что я ошибаюсь во время производства.

  • Наблюдения 1: я знаю, что получение потока для массива байтов может привести к лучшей производительности, чем получение массива байтов в кодировке Base64. Я не могу это контролировать. Вот каким должен быть API.
  • Наблюдение 2: Запрос к внешнему RESTful API (внутри метода ProcessOnExternalAPI выше) выполняется с использованием метода asyncPostAsync из класса System.Net.HttpClient.
  • Наблюдения 3: Больше всего меня беспокоит наличие рабочих потоков для ответа на запросы. Это не единственный сервис, на который отвечает мой API.
  • Наблюдения 4: я также проверил этот вопрос/ответ на SO: Использование async/await для нескольких задач Но там нет беспокойства по поводу основных рабочих потоков asp.net, что является моей главной заботой.
  • Наблюдение 5: Код создан на основе моих скромных знаний об асинхронном программировании на ASP.Net CORE, а также с опорой на эту документацию Microsoft: https://learn.microsoft.com/en-us/dotnet/csharp/ руководство по программированию/concepts/async/ Тем не менее, я хотел бы услышать от кого-то, у кого больше опыта работы с асинхронностью, об этом типе сценария.

каковы ваши критерии "наиболее эффективного способа"? вы измеряете его по количеству секунд, которое требуется для завершения этого метода API? или количество ресурсов, которые вы используете? или как-то иначе?

dee zg 12.12.2020 07:40

Примерно в это же время вы начнете бенчмаркинг.

TheGeneral 12.12.2020 07:43

@deezg наиболее эффективным способом является то, как он выполняет задачи параллельно, сохраняя при этом потоки, которые фактически обрабатывают основные запросы asp.net, разблокированными и доступными для обработки других запросов при выполнении ранее упомянутых задач.

Vitox 12.12.2020 07:44

@TheGeneral Итак, на данный момент нет очевидной лучшей реализации? Меня беспокоит, возможно, неспособность увидеть что-то явно неправильное в этом подходе или какое-то предположение, которое я сделал, что на самом деле неверно в этой реализации.

Vitox 12.12.2020 07:47

вы не делаете ничего "очевидно неправильного". как сказал @The General, сейчас самое время начать измерять и принимать решение об этом. что я, вероятно, сделал бы, если ваши бизнес-правила позволяют это, так это разгрузить запросы к стороннему API в какую-то очередь. Итак, вы сохраняете свое изображение на диск (или куда угодно) и добавляете ссылку на него в очередь. затем отдельный процесс считывает очередь и отправляет запросы стороннему API. его более сложная инфраструктура, но более эффективная и снимает нагрузку с вашего API.

dee zg 12.12.2020 07:50

@deezg Да. Я полностью согласен с тем, что асинхронный подход «запрос-ответ» был бы идеальным, но это невозможно. Это именно моя мотивация поддерживать рабочие потоки как можно более доступными, поскольку мне нужно поддерживать соединение в рабочем состоянии, а клиент продолжает ожидать ответа синхронно, и в то же время есть много других одновременных запросов...

Vitox 12.12.2020 07:56

Я думаю, что ваш дисковый ввод-вывод будет дросселироваться раньше всего. Таким образом, я бы рассмотрел возможность использования FileStream и управления размером буфера потока, если это ssd, я бы рассмотрел возможность установки для него максимального размера байта или выше. Также не забывайте об асинхронном флаге, если вы вызываете поток с асинхронными перегрузками. И да, если вы действительно серьезно относитесь к этому, вы, вероятно, захотите использовать шину сообщений и распределить рабочую нагрузку.

TheGeneral 12.12.2020 07:56

@TheGeneral К счастью, это единственная операция с интенсивным дисковым вводом-выводом в моем API, и она полностью соответствует возможностям дисков. Большой трафик на самом деле распределяется между службами этого API, поэтому эта дисковая операция выполняется только для части от общего числа запросов. Но это хорошая мысль (мы никогда не знаем, будет ли следующий вызов), я не думал об этом...

Vitox 12.12.2020 08:08

Как я уже сказал, меня больше всего беспокоят потоки, поскольку они нужны мне для других сервисов, которые получают значительно большую долю запросов. Поскольку мой опыт асинхронного программирования в среде веб-запросов не является абсолютно обширным, я хотел бы услышать от сообщества, если я на правильном пути...

Vitox 12.12.2020 08:12

Здесь у вас есть только 2 простых вызова. Вы уже поступаете правильно (при условии, что вы не хотите предпринимать более радикальные действия, как обсуждалось). Вы также можете использовать await Task.WhenAll, хотя заметной разницы во времени выполнения не будет.

TheGeneral 12.12.2020 08:40

Ваш код не самый эффективный, но, вероятно, самый масштабируемый. Вы можете сделать его более эффективным и, вероятно, менее масштабируемым, используя синхронный File.WriteAllBytes вместо File.WriteAllBytesAsync. API-интерфейсы асинхронной файловой системы неэффективно реализованы в .NET. В качестве примечания: не ожидая выполнения обеих задач с помощью Task.WhenAll, вы открываете возможность того, что первая задача станет запущенной и забытой в случае сбоя второй задачи.

Theodor Zoulias 12.12.2020 13:03
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
11
1 289
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Так ли это, или есть более эффективный способ сделать это?

С точки зрения потоков это правильно: ваш код выполняет две параллельные асинхронные операции.

Я предпочитаю использовать 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 быть доступным при выполнении асинхронных операций?

Vitox 14.12.2020 02:12

"если забуду" - нет; упомянутая проблема заключается в том, что если ProcessOnExternalAPI бросает, то taskSaveImage никогда не awaited; используя Task.WhenAll, вы гарантируете, что они оба всегда будут awaited, даже если один выкинет. Да, этот подход возвращает поток обратно в пул потоков в строке await Task.WhenAll.

Stephen Cleary 14.12.2020 03:47

Теперь я понимаю. Спасибо за объяснение!

Vitox 14.12.2020 05:20

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