Ограничивает ли Parallel.ForEach количество активных потоков?

107

Учитывая этот код:

var arrayStrings = new string[1000];
Parallel.ForEach<string>(arrayStrings, someString =>
{
    DoSomething(someString);
});

Будут ли все 1000 потоков появляться почти одновременно?

Джадер Диас
источник

Ответы:

149

Нет, он не запустит 1000 потоков - да, он ограничит количество используемых потоков. Parallel Extensions использует соответствующее количество ядер в зависимости от того, сколько у вас физически и сколько уже занято. Он распределяет работу для каждого ядра, а затем использует технику, называемую кражей работы, чтобы позволить каждому потоку эффективно обрабатывать свою собственную очередь и выполнять любой дорогостоящий межпоточный доступ только тогда, когда это действительно необходимо.

Посмотрите на блог PFX Team для нагрузок информации о том , как она распределяет работу и все виды других вопросов.

Обратите внимание, что в некоторых случаях вы также можете указать желаемую степень параллелизма.

Джон Скит
источник
2
Я использовал Parallel.ForEach (FilePathArray, path => ... чтобы прочитать сегодня около 24 000 файлов, создав по одному новому файлу для каждого файла, который я читал. Очень простой код. Похоже, что даже 6 потоков было достаточно, чтобы перегружать диск 7200 об / мин. Я читал со 100% -ным использованием. В течение нескольких часов я наблюдал, как параллельная библиотека выделяет более 8000 потоков. Я тестировал с помощью MaxDegreeOfParallelism, и, конечно же, 8000+ потоков исчезли. Я тестировал его несколько раз с тем же результат
Jake Drew
Он может запустить 1000 потоков для какого-то вырожденного DoSomething. (Как и в случае, когда я сейчас имею дело с проблемой в производственном коде, которая не смогла установить предел и породила 200+ потоков, тем самым выскочив пул соединений SQL. Я рекомендую устанавливать Max DOP для любой работы, которую нельзя тривиально обосновать о том, что это явно связано с процессором.)
user2864740
28

На одноядерной машине ... Parallel.ForEach разделы (фрагменты) коллекции, над которыми он работает, между несколькими потоками, но это число рассчитывается на основе алгоритма, который учитывает и, по-видимому, постоянно отслеживает работу, выполняемую потоков, которые он выделяет для ForEach. Поэтому, если часть тела ForEach вызывает длительные функции, связанные с вводом-выводом / блокирующие функции, которые заставят поток ждать, алгоритм создаст больше потоков и перераспределит коллекцию между ними . Если потоки завершаются быстро и не блокируются, например, потоками ввода-вывода, например, просто вычисляя некоторые числа,алгоритм будет увеличивать (или даже уменьшать) количество потоков до точки, в которой алгоритм считает оптимальной для пропускной способности (среднее время завершения каждой итерации) .

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

Я не считаю документацию по отмене и обработке синхронизирующих потоков очень полезной. Надеюсь, MS сможет предоставить лучшие примеры в MSDN.

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

Разработчик Microsoft
источник
1
"... если часть тела ForEach вызывает длительные функции блокировки, которые заставят поток ждать, алгоритм порождает больше потоков .." - В вырожденных случаях это означает, что может быть создано столько потоков, сколько разрешено на ThreadPool.
user2864740
2
Вы правы, для ввода-вывода он может выделить +100 потоков, так как я отлаживал сам
FindOutIslamNow
5

Он вырабатывает оптимальное количество потоков в зависимости от количества процессоров / ядер. Они не появятся все сразу.

Колин Маккей
источник
4

Отличный вопрос. В вашем примере уровень распараллеливания довольно низкий даже на четырехъядерном процессоре, но с некоторым ожиданием уровень распараллеливания может стать довольно высоким.

// Max concurrency: 5
[Test]
public void Memory_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);
        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Теперь посмотрим, что происходит, когда добавляется ожидающая операция для имитации HTTP-запроса.

// Max concurrency: 34
[Test]
public void Waiting_Operations()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    Parallel.ForEach<string>(arrayStrings, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

Я еще не внес никаких изменений, и уровень параллелизма / параллелизма резко подскочил. Ограничение параллелизма можно увеличить с помощью ParallelOptions.MaxDegreeOfParallelism.

// Max concurrency: 43
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(1000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

// Max concurrency: 391
[Test]
public void Test()
{
    ConcurrentBag<int> monitor = new ConcurrentBag<int>();
    ConcurrentBag<int> monitorOut = new ConcurrentBag<int>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(monitor.Count);

        System.Threading.Thread.Sleep(100000);

        monitor.TryTake(out int result);
        monitorOut.Add(result);
    });

    Console.WriteLine("Max concurrency: " + monitorOut.OrderByDescending(x => x).First());
}

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

Наконец, чтобы ответить на ваш вопрос, нет, вы не сможете запустить все потоки сразу. Используйте Parallel.Invoke, если вы хотите идеально выполнять параллельный вызов, например, при тестировании условий гонки.

// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623363344
// 636462943623368346
// 636462943623368346
// 636462943623373351
// 636462943623393364
// 636462943623393364
[Test]
public void Test()
{
    ConcurrentBag<string> monitor = new ConcurrentBag<string>();
    ConcurrentBag<string> monitorOut = new ConcurrentBag<string>();
    var arrayStrings = new string[1000];
    var options = new ParallelOptions {MaxDegreeOfParallelism = int.MaxValue};
    Parallel.ForEach<string>(arrayStrings, options, someString =>
    {
        monitor.Add(DateTime.UtcNow.Ticks.ToString());
        monitor.TryTake(out string result);
        monitorOut.Add(result);
    });

    var startTimes = monitorOut.OrderBy(x => x.ToString()).ToList();
    Console.WriteLine(string.Join(Environment.NewLine, startTimes.Take(10)));
}
Тимоти Гонсалес
источник