Мое серверное приложение принимает массивы байтов, представляющие данные изображения, и применяет к ним определенные преобразования, например изменение разрешения и т. д., а затем сохраняет эти измененные данные на диск.
В ходе некоторого анализа я обнаружил, что если имеется много одновременных запросов на использование этой утилиты преобразования, использование памяти серверным приложением резко возрастает, а на слабом оборудовании может возникнуть исключение OutOfMemoryException.
Чтобы временно решить эту проблему, я заключил операцию с интенсивным использованием памяти в блокирующий блок, примерно так:
public byte[] TransformImage(byte[] imageBytes)
{
lock (Lock)
{
// Do memory intensive work
}
}
Это работает, но также приводит к тому, что машины, которые теоретически могли обрабатывать несколько одновременных запросов, подобных этому, теперь застревают, имея возможность обрабатывать только один за раз, что потенциально влияет на UX.
Я думал, что могу назначить «Lock-Pool» или что-то в этом роде, который я могу расширить в соответствии с доступной памятью, но отсутствие источников, описывающих такое решение, намекает, что я могу использовать здесь неправильный подход.
Есть предложения?
@Оливер Большое спасибо. Кажется, это именно то, что мне нужно. Мне пришлось использовать переменную ThreadLocal<bool>, чтобы потоки не резервировали семафор несколько раз, поскольку у меня несколько точек входа, но в остальном это решение, похоже, работает. Я с радостью отмечу это как ответ, если вы опубликуете это как таковое.
Хотя предложение Оливера актуально, сколько оперативной памяти у сервера и сколько вы используете?
@MitchWheat К сожалению, я не могу раскрыть подробности, но существует множество аппаратных средств, на которых работает программное обеспечение. От небольших виртуальных серверов, у которых почти нет доступной памяти, до более производительных выделенных серверов, которые, конечно, не сталкиваются с этими проблемами.
Вы также можете попытаться прочитать, сколько памяти доступно на главном компьютере, и таким образом распределить свой семафор.
@TheodorZoulias Спасибо за предупреждение. Я исправил это :) Распределение семафоров в соответствии с доступной памятью - это то, о чем я рассмотрю, если оно появится снова. Возможно, ограничить количество доступных семафоров одним на машинах со значительно малым объемом доступной памяти. Но в этот момент нам, вероятно, следует пересмотреть возможность позволить этому серверу обрабатывать изображения, поскольку это действительно кажется довольно ресурсоемким.





Если вам необходимо ограничить одновременное использование одного метода более чем одним, правильным классом потенциально будет 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(). Похоже, вы выпускаете его преждевременно.
@TheodorZoulias Хороший улов. Я как бы модифицирую это в коде, написанном без учета параллелизма. Согласен с вами, но в моем случае это работает нормально. TransformImage перегружен, и некоторые из перегрузок вызывают друг друга. Все их тела обернуты в тот же try/finally, что и в примере. Признаюсь, я не совсем доволен этим, как бы я к этому ни подошел.
Ну, реализация реентеранта Semaphore не является частью вопроса, поэтому ваш ответ, вероятно, здесь не по теме. Так что мне придется понизить голосование, извините. Даже если бы это было по теме, эта реализация нарушает ожидания большинства разработчиков относительно того, как должен работать реентерант Semaphore, поэтому мне все равно пришлось бы ее отвергнуть (если только это нетипичное поведение не было явно запрошено в вопросе). Если вас действительно интересует реентерант Semaphore, актуальный вопрос здесь, не обязательно задавать новый.
Кажется, это работа для SemaphoreSlim. Если ваше программное обеспечение предоставляет некоторую конфигурацию, администратор может сказать, сколько параллельных преобразований можно выполнить, и вы используете это число в векторе семафора.