Параллельный .ForEach против Task.Factory.StartNew

В чем разница между приведенными ниже фрагментами кода? Не будут ли использоваться потоки threadpool?

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

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

Ответ 1

Первый вариант гораздо лучше.

Parallel.ForEach, внутри, использует Partitioner<T>, чтобы распространять вашу коллекцию в рабочие элементы. Он не будет выполнять одну задачу для каждого элемента, а скорее, чтобы уменьшить задействованные служебные данные.

Второй вариант запланирует один Task для каждого элемента в вашей коллекции. Хотя результаты будут (почти) одинаковыми, это приведет к гораздо более накладным расходам, чем необходимо, особенно для больших коллекций, и приведет к замедлению общего времени выполнения.

FYI. Используемый Partitioner можно контролировать, используя соответствующие перегрузки в Parallel.ForEach, если это необходимо. Подробнее см. Пользовательские разделители в MSDN.

Основное различие, во время выполнения, второе будет действовать асинхронно. Это можно продублировать с помощью Parallel.ForEach, выполнив следующие действия:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

Выполняя это, вы по-прежнему пользуетесь разделителями, но не блокируете до завершения операции.

Ответ 2

Я сделал небольшой эксперимент по запуску метода "1000000000" раз с "Parallel.For" и одним с объектами "Задача".

Я измерил время процессора и нашел Parallel более эффективным. Parallel.For делит вашу задачу на небольшие рабочие элементы и выполняет их на всех ядрах в оптимальном порядке. При создании множества объектов задач (FYI TPL будет использовать пул потоков внутри) будет перемещать каждое выполнение по каждой задаче, создавая больше стресса в ящике, что видно из приведенного ниже эксперимента.

Я также создал небольшое видео, которое объясняет базовый TPL, а также продемонстрировало, как Parallel.For использует ваше ядро ​​более эффективно http://www.youtube.com/watch?v=No7QqSc5cl8 по сравнению к обычным задачам и потокам.

Эксперимент 1

Parallel.For(0, 1000000000, x => Method1());

Эксперимент 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

Processor time comparison

Ответ 3

Parallel.ForEach будет оптимизировать (возможно, даже не запускать новые потоки) и блокировать до тех пор, пока цикл не будет завершен, а Task.Factory явно создаст новый экземпляр задачи для каждого элемента и вернется до их завершения (асинхронные задачи). Parallel.Foreach намного эффективнее.

Ответ 4

На мой взгляд, наиболее реалистичным сценарием является то, что задачи выполняют тяжелую операцию. Подход Shivprasad больше фокусируется на создании объекта/распределении памяти, чем на самом вычислении. Я провел исследование, назвав следующий метод:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

Выполнение этого метода занимает около 0,5 сек.

Я назвал его 200 раз, используя Parallel:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

Затем я назвал его 200 раз, используя старомодный способ:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

Первый случай завершен в 26656 мс, второй - в 24478мс. Я повторил это много раз. Каждый раз, когда второй подход маргиналист быстрее.