Ответ
Ограничение параллелизма — ключевая задача для предотвращения истощения ресурсов (потоков, соединений с БД) и контроля нагрузки. В 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— применяйте только в специфичных случаях, когда нужно жестко ограничить ресурсы всего приложения.