Ответ
Ограничение параллелизма — ключевая задача для предотвращения истощения ресурсов (потоков, соединений с БД) и контроля нагрузки. В C# есть несколько подходов в зависимости от сценария.
1. Для асинхронных операций (I/O-bound задачи) — SemaphoreSlim
Наиболее гибкий и современный способ. Он асинхронно блокирует выполнение, не занимая потоков в ожидании.
// Ограничиваем обработку до 5 параллельных HTTP-запросов или операций с файлами
private static readonly SemaphoreSlim _throttler = new SemaphoreSlim(5);
public async Task ProcessItemsAsync(IEnumerable<Uri> urls)
{
var tasks = urls.Select(async url =>
{
// Ждем, пока не освободится слот (максимум 5 слотов)
await _throttler.WaitAsync();
try
{
// Выполняем ресурсоемкую операцию (например, HTTP-запрос)
return await _httpClient.GetStringAsync(url);
}
finally
{
// Освобождаем слот для следующей задачи
_throttler.Release();
}
});
await Task.WhenAll(tasks);
}
2. Для CPU-bound задач — ParallelOptions с MaxDegreeOfParallelism
Используется для параллельной обработки данных, где работа ведется в пуле потоков.
var data = Enumerable.Range(1, 1000).ToList();
var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; // Ограничиваем числом ядер
Parallel.ForEach(data, options, item =>
{
// Интенсивные вычисления (например, обработка изображений)
var result = ComputeHeavyOperation(item);
// ...
});
3. Для задач, планируемых через Task — ActionBlock<T> (TPL Dataflow)
Библиотека System.Threading.Tasks.Dataflow предоставляет мощные примитивы для конвейерной обработки.
// Создаем блок, который выполняет не более 3 задач одновременно
var actionBlock = new ActionBlock<string>(async url =>
{
await ProcessUrlAsync(url);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3 });
// Отправляем данные на обработку
foreach (var url in urlList)
{
actionBlock.Post(url);
}
// Сообщаем блоку, что данные закончились, и ждем завершения обработки
actionBlock.Complete();
await actionBlock.Completion;
4. Глобальное ограничение пула потоков (используйте с осторожностью!)`
Меняет настройки для всего домена приложения. Может негативно повлиять на работу библиотек.
// Устанавливаем максимум 10 рабочих потоков и 10 потоков ввода-вывода
ThreadPool.SetMaxThreads(10, 10);
// Получить текущие лимиты можно через ThreadPool.GetMaxThreads(...)
Выбор подхода:
SemaphoreSlim— лучший выбор для большинства асинхронных сценариев (запросы к API, БД, файлам).ParallelOptions— для параллельных вычислений (циклыParallel.ForEach).ActionBlock<T>— для сложных конвейеров обработки данных с четким контролем параллелизма.- Глобальный
ThreadPool— применяйте только в специфичных случаях, когда нужно жестко ограничить ресурсы всего приложения.
Ответ 18+ 🔞
Давай разберем эту тему про ограничение параллелизма, а то народ как с цепи срывается — запускают десять тысяч потоков на трёхъядерном процессоре, а потом удивляются, почему комп тупит, как пьяный трактор.
Смотри, вариантов несколько, и каждый для своей ситуации.
1. Для всяких асинхронных дел (запросы, файлы, базы) — SemaphoreSlim
Это, блядь, твой лучший друг. Он не тупо жрёт поток, пока ждёт, а по-человечески асинхронно блокируется. Представь, что у тебя турникет в метро — больше пяти человек одновременно не пролезет.
// Допустим, больше 5 запросов одновременно — уже пиздец серверу
private static readonly SemaphoreSlim _турникет = new SemaphoreSlim(5);
public async Task ОбработатьВсёAsync(IEnumerable<Uri> ссылки)
{
var задачи = ссылки.Select(async url =>
{
// Стоим в очереди, ждём свободного слота
await _турникет.WaitAsync();
try
{
// Вот тут уже делаем свою грязную работу
return await _httpClient.GetStringAsync(url);
}
finally
{
// Важно! Вышли — освободили место для следующего, а то все встанут
_турникет.Release();
}
});
await Task.WhenAll(задачи);
}
2. Для тяжёлых вычислений (CPU-bound) — ParallelOptions
Тут уже SemaphoreSlim не катит, потому что работа идёт в пуле потоков. Используй Parallel.ForEach с настройкой.
var данные = Enumerable.Range(1, 10000).ToList();
var настройки = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; // Не умничай, больше ядер — не значит быстрее
Parallel.ForEach(данные, настройки, число =>
{
// Что-то ебёт твой процессор, например, шифрует или картинки масштабирует
var результат = ВычислитьЧтоТоОченьТяжёлое(число);
// ...
});
3. Для конвейерной обработки — ActionBlock<T> (библиотека Dataflow)
Штука мощная, но чуть посложнее. Представь конвейер на заводе: детали едут, а рабочих мест для их обработки — всего три.
// Создаём блок, который не более 3 URL'ов одновременно обрабатывает
var рабочийБлок = new ActionBlock<string>(async url =>
{
await ОбработатьСсылкуAsync(url);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3 });
// Кидаем в него работу
foreach (var url in списокСсылок)
{
рабочийБлок.Post(url);
}
// Говорим: "Всё, мужики, новых деталей не будет, доделывайте что есть"
рабочийБлок.Complete();
await рабочийБлок.Completion; // Ждём, пока последняя деталь не уедет с конвейера
4. Глобальный контроль пула потоков (ОСТОРОЖНО, СТРЕЛЯНОЕ ОРУЖИЕ!)
Вот это — уже ядрёна вошь. Меняешь настройки на всё приложение разом. Сделаешь не так — половина библиотек встанет колом, потому что им потоки не выделяются.
// Говорим всему пулу: "Больше 10 рабочих потоков и 10 потоков ввода-вывода — ни-ни"
ThreadPool.SetMaxThreads(10, 10);
// Текущие лимиты можно посмотреть через ThreadPool.GetMaxThreads(...)
Короче, итог:
- Делаешь запросы к API, БД, файлам? —
SemaphoreSlim, идиотская ошибка. - Гоняешь циклы с вычислениями? —
ParallelOptionsвParallel.ForEach. - Строишь сложный конвейер, где данные текут этапами? —
ActionBlock<T>из Dataflow. - Хочешь глобально придушить всё приложение, потому что знаешь, что делаешь? —
ThreadPool.SetMaxThreads. Но если не уверен — даже не трогай, а то будет тебе хиросима.