Parallel.ForEach vs Task.Factory.StartNew
В чем разница между приведенными ниже фрагментами кода? Разве оба не будут использовать потоки потоков?
Например, если я хочу вызвать функцию для каждого элемента в коллекции,
Parallel.ForEach<Item>(items, item => DoSomething(item));
vs
foreach(var item in items)
{
Task.Factory.StartNew(() => DoSomething(item));
}
4 ответа
Первый вариант намного лучше.
Parallel.ForEach, внутренне, использует Partitioner<T>
распределить вашу коллекцию по рабочим элементам. Он не будет выполнять одну задачу для каждого элемента, а вместо этого выполнит пакетную обработку, чтобы снизить накладные расходы.
Второй вариант будет график одного Task
за элемент в вашей коллекции. Хотя результаты будут (почти) одинаковыми, это приведет к гораздо большим затратам, чем необходимо, особенно для больших коллекций, и замедлит общее время выполнения.
К сведению - используемым Разделителем можно управлять с помощью соответствующих перегрузок в Parallel.ForEach, если это необходимо. Для получения дополнительной информации см. Пользовательские разделы на MSDN.
Основным отличием во время выполнения является то, что второе будет работать асинхронно. Это может быть продублировано с помощью Parallel.ForEach, выполнив:
Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));
Делая это, вы все еще пользуетесь разделителями, но не блокируете, пока операция не завершится.
Я провел небольшой эксперимент по запуску метода "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();
}
Parallel.ForEach оптимизирует (может даже не запускать новые потоки) и блокирует, пока цикл не завершится, а Task.Factory явно создаст новый экземпляр задачи для каждого элемента и вернется до того, как они будут завершены (асинхронные задачи). Parallel.Foreach гораздо эффективнее.
На мой взгляд, наиболее реалистичный сценарий - это когда задачи требуют тяжелой работы. Подход 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мс. Я повторил это много раз. Каждый раз второй подход незначительно быстрее.