Parallel.ForEach vs Task.Factory.StartNew

267

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

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

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

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}
stackoverflowuser
источник

Ответы:

302

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

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

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

К сведению - используемым Разделителем можно управлять, используя соответствующие перегрузки для Parallel.ForEach , если это необходимо. Для получения дополнительной информации см. Пользовательские разделы на MSDN.

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

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

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

Рид Копси
источник
8
IIRC, разделение по умолчанию, выполняемое Parallel.ForEach, также учитывает количество доступных аппаратных потоков, избавляя вас от необходимости выбирать оптимальное количество задач для запуска. Ознакомьтесь со статьей Microsoft « Шаблоны параллельного программирования» ; в нем есть отличные объяснения всего этого.
Мал Росс
2
@Mal: вроде ... Это на самом деле не Partitioner, а работа TaskScheduler. TaskScheduler по умолчанию использует новый ThreadPool, который теперь очень хорошо справляется с этим.
Рид Копси
Спасибо. Я знал, что я должен был оставить в "Я не эксперт, но ..." предостережение. :)
Мал Росс
@ReedCopsey: Как прикрепить задачи, запущенные через Parallel.ForEach, к задаче-обертке? Так что, когда вы вызываете .Wait () для задачи-оболочки, она зависает до тех пор, пока задачи, выполняющиеся параллельно, не будут завершены?
Константин Таркус
1
@Tarkus Если вы делаете несколько запросов, вам лучше использовать HttpClient.GetString в каждом рабочем элементе (в вашем параллельном цикле). Нет причин помещать асинхронную опцию в уже параллельный цикл, как правило ...
Рид Копси,
89

Я провел небольшой эксперимент по запуску метода «1 000 000 000 (один миллиард)» раз с «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();
}

Сравнение времени процессора

Шивпрасад Койрала
источник
Это было бы более эффективно, и причина в том, что создание потоков стоит дорого. Эксперимент 2 - очень плохая практика.
Тим
@ Георгий - пожалуйста, поговори о том, что плохо.
Шивпрасад Коирала
3
Прошу прощения, моя ошибка, я должен был уточнить. Я имею в виду создание задач в цикле до 1000000000. Накладные расходы невообразимы. Не говоря уже о том, что Parallel не может создавать более 63 задач одновременно, что делает его гораздо более оптимизированным в данном случае.
Георгий-это
Это верно для 1000000000 задач. Однако, когда я обрабатываю изображение (многократно увеличивая фрактал) и выполняю Parallel.For, многие ядра бездействуют в ожидании завершения последних потоков. Чтобы сделать это быстрее, я сам разбил данные на 64 рабочих пакета и создал для них задачи. (Затем Task.WaitAll будет ожидать завершения.) Идея состоит в том, чтобы незанятые потоки выбирали рабочий пакет, чтобы помочь завершить работу, вместо того, чтобы ждать 1-2 потока, чтобы завершить назначенный им блок (Parallel.For).
Тедд Хансен
1
Что делает Mehthod1()в этом примере?
Запнологика
17

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

Sogger
источник
11

На мой взгляд, наиболее реалистичный сценарий - это когда задачи требуют тяжелой работы. Подход 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мс. Я повторил это много раз. Каждый раз второй подход незначительно быстрее.

user1089583
источник
Использование Parallel.For является старомодным способом. Использование Task рекомендуется для единиц работы, которые не являются единообразными. Microsoft MVP и разработчики TPL также отмечают, что при использовании Задач будет более эффективно использовать потоки, поскольку они блокируют столько же, ожидая завершения других модулей.
Suncat2000