Parallel.Foreach vs foreach w pojedynczym Task'u

0

Hej,

Pomijając sens tych dwóch funkcji, StartParallel wykonuje się ok 3 razy dłużej niż StartTask.

private const int RANGE = 10000000;

private static void StartParallel()
{
    var numbers = Enumerable.Range(1, RANGE).ToList();
    var copy = new BlockingCollection<int>();

    Task.Run(() =>
    {
        Parallel.ForEach(numbers, (number) =>
        {
            copy.Add((int)(Math.Sqrt(number * Math.PI)));
        });
        copy.CompleteAdding();
    }).Wait();
}

private static void StartTask()
{
    var numbers = Enumerable.Range(1, RANGE).ToList();
    var copy = new BlockingCollection<int>();

    Task.Run(() =>
    {
        foreach(var number in numbers)
        {
            copy.Add((int)(Math.Sqrt(number * Math.PI)));
        }
        copy.CompleteAdding();
    }).Wait();
}

Jak mówi strona Microsoft:

Data parallelism refers to scenarios in which the same operation is performed concurrently (that is, in parallel) on elements in a source collection or array. In data parallel operations, the source collection is partitioned so that multiple threads can operate on different segments concurrently.

Jeśli dobrze to rozumiem Parallel.Foreach dzieli dane z kolekcji na "kawałki" dzięki czemu wiele wątków może wykonywać na nich operację równocześnie.
Ktoś może w przystępny sposób wyjaśnić różnicę między tymi funkcjami, skąd wynika taka rozbieżność czasowa między nimi i kiedy powinno używać się danego rozwiązania?

5

Parallel.ForEach - jak już przeczytałeś, dzieli całą operację na wątki. Dzięki czemu mogą się wykonywać faktycznie równocześnie (przy założeniu, że masz kilka rdzeni na procesorze). I mogłoby się wydawać, że taka operacja będzie szybsza... Nie zawsze.

W Twoim przypadku StartTask wykona się szybciej. Dzieje się tak dlatego, że przy StartParallel używasz BlockingCollection, które musi być synchronizowane. I tutaj gubisz swoje sekundy. Synchronizacja też coś tam trwa. W przypadku Taska, wszystko wykonujesz na jednym wątku, a więc synchronizacja nie jest potrzebna. Pamiętaj, że Task to nie zawsze oznacza nowy wątek.

Równoległości używa się raczej do obliczeń. Najlepiej sprawdza się tam, kiedy możemy zminimalizować synchronizowanie.

0

Dzięki, teraz jest to dużo jaśniejsze!

4

A mówiąc kolokwialnie, napisałeś kiepski kod równoległy, jak chcesz by wynikiem była cała przetworzona tablica to lepszy będzie Parallel.For i w ogóle nie potrzebujesz synchronizacji:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var numbers = Enumerable.Range(1, RANGE).ToList();
            

            var s1 = new Stopwatch();
            s1.Start();
            StartTask(numbers);            
            Console.WriteLine(s1.ElapsedMilliseconds);
            s1.Restart();
            StartParallel(numbers);
            Console.WriteLine(s1.ElapsedMilliseconds);
        }

        private const int RANGE = 100000000;

        private static void StartParallel(List<int> numbers)
        {
            var copy = new int[RANGE];
            Parallel.For(0, RANGE, (index) =>
            {
                copy[index] = (int)(Math.Sqrt(numbers[index] * Math.PI));
            });
        }

        private static void StartTask(List<int> numbers)
        {          
            var copy = new int[RANGE];
           
            for(int i = 0; i < RANGE; ++i)
            {
                copy[i] = (int)(Math.Sqrt(numbers[i] * Math.PI));
            }
        }
    }
}

0

To fakt, ale jak napisałem wcześniej sensowność tych funkcji nie ma tu znaczenia.
Rozumiem że dla Parallel.For synchronizacja nie jest potrzebna bo dba o to sama by dane np. nie wyszły poza zakres tablicy lub odwołały się kilka razy do tego samego numeru.

1 użytkowników online, w tym zalogowanych: 0, gości: 1