Краткий ответ: нет , вы не должны просто использовать Parallel.ForEach
или связанные конструкции в каждом цикле, который вы можете.Параллель имеет некоторые издержки, что не оправдано в циклах с небольшим количеством быстрых итераций.Кроме того, break
значительно сложнее внутри этих циклов.
Parallel.ForEach
- это запрос на планирование цикла, когда планировщик задач сочтет нужным, основываясь на количестве итераций в цикле, количестве ядер ЦП наоборудование и текущая нагрузка на это оборудование.Фактическое параллельное выполнение не всегда гарантируется и менее вероятно, если имеется меньше ядер, число итераций мало и / или текущая нагрузка высока.
См. Также Ограничивает ли Parallel.ForEach ограничениеколичество активных потоков? и Использует ли Parallel.For одну задачу на итерацию?
Длинный ответ:
Мы можем классифицироватьцикл по тому, как они падают на две оси:
- От нескольких итераций до многих итераций.
- Каждая итерация проходит быстро, и каждая итерация медленная.
Третий фактор - это если задачи сильно различаются по длительности - например, если вы вычисляете точки на множестве Мандельбротанекоторые точки быстро рассчитываются, некоторые занимают гораздо больше времени.
Когда существует несколько быстрых итераций, вероятно, не стоит использовать распараллеливание каким-либо образом, скорее всего, оно будет медленнее из-за накладных расходов.Даже если распараллеливание ускоряет конкретный небольшой быстрый цикл, оно вряд ли будет представлять интерес: выигрыш будет небольшим, и это не является узким местом для производительности в вашем приложении, поэтому оптимизируйте его на удобочитаемость, а не производительность.
Где циклимеет очень мало медленных итераций, и вам нужен больший контроль, вы можете рассмотреть возможность использования Задач для их обработки по следующим направлениям:
var tasks = new List<Task>(actions.Length);
foreach(var action in actions)
{
tasks.Add(Task.Factory.StartNew(action));
}
Task.WaitAll(tasks.ToArray());
Там, где много итераций, Parallel.ForEach
находится в его элементе.
Документация Microsoft гласит, что
При выполнении параллельного цикла TPL разделяет источник данных, чтобы цикл мог работать одновременно на нескольких частях.За кулисами планировщик задач разделяет задачу на основе системных ресурсов и рабочей нагрузки.Когда это возможно, планировщик перераспределяет работу между несколькими потоками и процессорами, если рабочая нагрузка становится неуравновешенной.
Это разделение и динамическое перепланирование будет труднее сделать эффективно, так как число итераций цикла уменьшается,и является более необходимым, если итерации различаются по продолжительности и при наличии других задач, выполняющихся на одном и том же компьютере.
Я запустил некоторый код.
Результаты теста нижепокажите машину, на которой больше ничего не запущено, и никаких других потоков из пула потоков .Net.Это не типично (на самом деле в сценарии с веб-сервером это крайне нереально).На практике вы можете не увидеть распараллеливания с небольшим числом итераций.
Тестовый код:
namespace ParallelTests
{
class Program
{
private static int Fibonacci(int x)
{
if (x <= 1)
{
return 1;
}
return Fibonacci(x - 1) + Fibonacci(x - 2);
}
private static void DummyWork()
{
var result = Fibonacci(10);
// inspect the result so it is no optimised away.
// We know that the exception is never thrown. The compiler does not.
if (result > 300)
{
throw new Exception("failed to to it");
}
}
private const int TotalWorkItems = 2000000;
private static void SerialWork(int outerWorkItems)
{
int innerLoopLimit = TotalWorkItems / outerWorkItems;
for (int index1 = 0; index1 < outerWorkItems; index1++)
{
InnerLoop(innerLoopLimit);
}
}
private static void InnerLoop(int innerLoopLimit)
{
for (int index2 = 0; index2 < innerLoopLimit; index2++)
{
DummyWork();
}
}
private static void ParallelWork(int outerWorkItems)
{
int innerLoopLimit = TotalWorkItems / outerWorkItems;
var outerRange = Enumerable.Range(0, outerWorkItems);
Parallel.ForEach(outerRange, index1 =>
{
InnerLoop(innerLoopLimit);
});
}
private static void TimeOperation(string desc, Action operation)
{
Stopwatch timer = new Stopwatch();
timer.Start();
operation();
timer.Stop();
string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed);
Console.WriteLine(message);
}
static void Main(string[] args)
{
TimeOperation("serial work: 1", () => Program.SerialWork(1));
TimeOperation("serial work: 2", () => Program.SerialWork(2));
TimeOperation("serial work: 3", () => Program.SerialWork(3));
TimeOperation("serial work: 4", () => Program.SerialWork(4));
TimeOperation("serial work: 8", () => Program.SerialWork(8));
TimeOperation("serial work: 16", () => Program.SerialWork(16));
TimeOperation("serial work: 32", () => Program.SerialWork(32));
TimeOperation("serial work: 1k", () => Program.SerialWork(1000));
TimeOperation("serial work: 10k", () => Program.SerialWork(10000));
TimeOperation("serial work: 100k", () => Program.SerialWork(100000));
TimeOperation("parallel work: 1", () => Program.ParallelWork(1));
TimeOperation("parallel work: 2", () => Program.ParallelWork(2));
TimeOperation("parallel work: 3", () => Program.ParallelWork(3));
TimeOperation("parallel work: 4", () => Program.ParallelWork(4));
TimeOperation("parallel work: 8", () => Program.ParallelWork(8));
TimeOperation("parallel work: 16", () => Program.ParallelWork(16));
TimeOperation("parallel work: 32", () => Program.ParallelWork(32));
TimeOperation("parallel work: 64", () => Program.ParallelWork(64));
TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000));
TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000));
TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000));
Console.WriteLine("done");
Console.ReadLine();
}
}
}
результаты на 4-ядерном компьютере с Windows 7:
serial work: 1 took 00:02.31
serial work: 2 took 00:02.27
serial work: 3 took 00:02.28
serial work: 4 took 00:02.28
serial work: 8 took 00:02.28
serial work: 16 took 00:02.27
serial work: 32 took 00:02.27
serial work: 1k took 00:02.27
serial work: 10k took 00:02.28
serial work: 100k took 00:02.28
parallel work: 1 took 00:02.33
parallel work: 2 took 00:01.14
parallel work: 3 took 00:00.96
parallel work: 4 took 00:00.78
parallel work: 8 took 00:00.84
parallel work: 16 took 00:00.86
parallel work: 32 took 00:00.82
parallel work: 64 took 00:00.80
parallel work: 1k took 00:00.77
parallel work: 10k took 00:00.78
parallel work: 100k took 00:00.77
done
Выполнение кода, скомпилированного в .Net 4 и .Net 4.5, дает практически одинаковые результаты.
Последовательные работы выполняются одинаково.Неважно, как вы нарезаете его, оно выполняется примерно за 2,28 секунды.
Параллельная работа с 1 итерацией немного дольше, чем никакой параллелизм.2 элемента короче, так что 3 и с 4 или более итерациями все около 0,8 секунд.
Используются все ядра, но не с эффективностью 100%.Если бы последовательная работа была разделена на 4 части без дополнительных затрат, она бы завершилась за 0,57 секунды (2,28 / 4 = 0,57).
В других сценариях я вообще не видел ускорения с параллельными 2-3 итерациями.У вас нет точного контроля над этим с Parallel.ForEach
, и алгоритм может решить разделить их на 1 блок и запустить на 1 ядре, если машина занята.