«Пулы блокировки» или другие решения для обработки операций с интенсивным использованием памяти в серверной части .NET Framework?

Мое серверное приложение принимает массивы байтов, представляющие данные изображения, и применяет к ним определенные преобразования, например изменение разрешения и т. д., а затем сохраняет эти измененные данные на диск.

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

Чтобы временно решить эту проблему, я заключил операцию с интенсивным использованием памяти в блокирующий блок, примерно так:

        public byte[] TransformImage(byte[] imageBytes)
        {
            lock (Lock)
            {
                // Do memory intensive work
            }
        }

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

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

Есть предложения?

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

Oliver 20.08.2024 11:55

@Оливер Большое спасибо. Кажется, это именно то, что мне нужно. Мне пришлось использовать переменную ThreadLocal<bool>, чтобы потоки не резервировали семафор несколько раз, поскольку у меня несколько точек входа, но в остальном это решение, похоже, работает. Я с радостью отмечу это как ответ, если вы опубликуете это как таковое.

Allu 20.08.2024 12:28

Хотя предложение Оливера актуально, сколько оперативной памяти у сервера и сколько вы используете?

Mitch Wheat 20.08.2024 12:38

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

Allu 20.08.2024 12:48

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

beautifulcoder 20.08.2024 15:39

@TheodorZoulias Спасибо за предупреждение. Я исправил это :) Распределение семафоров в соответствии с доступной памятью - это то, о чем я рассмотрю, если оно появится снова. Возможно, ограничить количество доступных семафоров одним на машинах со значительно малым объемом доступной памяти. Но в этот момент нам, вероятно, следует пересмотреть возможность позволить этому серверу обрабатывать изображения, поскольку это действительно кажется довольно ресурсоемким.

Allu 20.08.2024 15:54
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
6
51
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Если вам необходимо ограничить одновременное использование одного метода более чем одним, правильным классом потенциально будет SemaphoreSlim:

Представляет облегченную альтернативу Semaphore, которая ограничивает количество потоков, которые могут одновременно обращаться к ресурсу или пулу ресурсов.

Прямо из упомянутой документации приведен пример того, как можно ограничить максимальное количество одновременных задач, которые будут выполнять действие:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    private static SemaphoreSlim semaphore;
    // A padding interval to make the output more orderly.
    private static int padding;

    public static void Main()
    {
        // Create the semaphore.
        semaphore = new SemaphoreSlim(0, 3);
        Console.WriteLine("{0} tasks can enter the semaphore.",
                          semaphore.CurrentCount);
        Task[] tasks = new Task[5];

        // Create and start five numbered tasks.
        for (int i = 0; i <= 4; i++)
        {
            tasks[i] = Task.Run(() =>
            {
                // Each task begins by requesting the semaphore.
                Console.WriteLine("Task {0} begins and waits for the semaphore.",
                                  Task.CurrentId);
                
                int semaphoreCount;
                semaphore.Wait();
                try
                {
                    Interlocked.Add(ref padding, 100);

                    Console.WriteLine("Task {0} enters the semaphore.", Task.CurrentId);

                    // The task just sleeps for 1+ seconds.
                    Thread.Sleep(1000 + padding);
                }
                finally {
                    semaphoreCount = semaphore.Release();
                }
                Console.WriteLine("Task {0} releases the semaphore; previous count: {1}.",
                                  Task.CurrentId, semaphoreCount);
            });
        }

        // Wait for half a second, to allow all the tasks to start and block.
        Thread.Sleep(500);

        // Restore the semaphore count to its maximum value.
        Console.Write("Main thread calls Release(3) --> ");
        semaphore.Release(3);
        Console.WriteLine("{0} tasks can enter the semaphore.",
                          semaphore.CurrentCount);
        // Main thread waits for the tasks to complete.
        Task.WaitAll(tasks);

        Console.WriteLine("Main thread exits.");
    }
}
// The example displays output like the following:
//       0 tasks can enter the semaphore.
//       Task 1 begins and waits for the semaphore.
//       Task 5 begins and waits for the semaphore.
//       Task 2 begins and waits for the semaphore.
//       Task 4 begins and waits for the semaphore.
//       Task 3 begins and waits for the semaphore.
//       Main thread calls Release(3) --> 3 tasks can enter the semaphore.
//       Task 4 enters the semaphore.
//       Task 1 enters the semaphore.
//       Task 3 enters the semaphore.
//       Task 4 releases the semaphore; previous count: 0.
//       Task 2 enters the semaphore.
//       Task 1 releases the semaphore; previous count: 0.
//       Task 3 releases the semaphore; previous count: 0.
//       Task 5 enters the semaphore.
//       Task 2 releases the semaphore; previous count: 1.
//       Task 5 releases the semaphore; previous count: 2.
//       Main thread exits.

Основываясь на ответе Оливера, я на данный момент остановился на следующем решении:

private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(3, 3);
private readonly ThreadLocal<bool> SemaphoreHeld = new ThreadLocal<bool>(() => false);

public byte[] TransformImage(byte[] imageBytes)
{
    AcquireSemaphore();
    try
    {
        // Do memory intensive work
    }
    finally
    {
        ReleaseSemaphore();
    }
}

private void AcquireSemaphore()
{
    if (!SemaphoreHeld.Value)
    {
        Semaphore.Wait();
        SemaphoreHeld.Value = true;
    }
}

private void ReleaseSemaphore()
{
    if (SemaphoreHeld.Value)
    {
        Semaphore.Release();
        SemaphoreHeld.Value = false;
    }
}

В некоторых методах я вызываю другие функции, которые получают тот же семафор. Использование ThreadLocal позволяет мне проверить, получил ли текущий поток его уже, а затем пропустить его повторное получение.

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

Какова цель поля ThreadLocal<bool>? Вы пытаетесь сделать SemaphoreSlimреентерабельным? Если да, то я думаю, что ваша реализация ошибочна. Реентерабельная блокировка должна быть фактически снята, когда было выдано количество Release(), равное числу Acquire(). Похоже, вы выпускаете его преждевременно.

Theodor Zoulias 20.08.2024 16:57

@TheodorZoulias Хороший улов. Я как бы модифицирую это в коде, написанном без учета параллелизма. Согласен с вами, но в моем случае это работает нормально. TransformImage перегружен, и некоторые из перегрузок вызывают друг друга. Все их тела обернуты в тот же try/finally, что и в примере. Признаюсь, я не совсем доволен этим, как бы я к этому ни подошел.

Allu 20.08.2024 23:24

Ну, реализация реентеранта Semaphore не является частью вопроса, поэтому ваш ответ, вероятно, здесь не по теме. Так что мне придется понизить голосование, извините. Даже если бы это было по теме, эта реализация нарушает ожидания большинства разработчиков относительно того, как должен работать реентерант Semaphore, поэтому мне все равно пришлось бы ее отвергнуть (если только это нетипичное поведение не было явно запрошено в вопросе). Если вас действительно интересует реентерант Semaphore, актуальный вопрос здесь, не обязательно задавать новый.

Theodor Zoulias 21.08.2024 02:38

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