Когда в очередной раз понадобилось запилить параллельную обработку одной задачи, неожиданно открыл для себя Parallel.ForEach. Там уже всё сделано, включая ограничение на максимальное количество одновременно работающих потоков. Я восхитился, выкинул все свои многопоточные костыли и заюзал этот прекрасный метод.

Всё просто:

public void PrepareToDoParallelStuff()
{
    // список чего-нибудь, например идентификаторов из какой-то таблицы
    List<string> someVals = new List<string>();
    for (int i = 1; i < 999; i++)
    {
        someVals.Add(i.ToString());
    }

    // для замеров времени обработки
    Stopwatch sw = new Stopwatch();
    sw.Start();

    // создаём параметры для нашей параллельной обработки
    ParallelOptions options = new ParallelOptions();
    // а конкретно - максимальное количество одновременно работающих потоков
    options.MaxDegreeOfParallelism = 5;
    // а тут запускаем сам многопоточный foreach:
    // - someVals: список идентификаторов, который мы определили выше
    // - options: настройки параллельной обработки
    // - doParallelStuff: делегат метода, который будет выполняться параллельно
    Parallel.ForEach<string>(someVals, options, DoParallelStuff);

    sw.Stop();
    Console.WriteLine(string.Format("Прошло времени: {0}", sw.ElapsedMilliseconds));
}

// вот этот метод и будет выполняться параллельно
void DoParallelStuff(string someVal)
{
    Interlocked.Increment(ref threadsCount);
    Console.WriteLine("{0}, {1}", threadsCount, someVal);
    Interlocked.Decrement(ref threadsCount);
}

// это просто счётчик работающих потоков
private int threadsCount = 0;