poniedziałek, 26 kwietnia 2010

Task Parallel Library cz. 2

Aby możliwe było korzystanie z nowych udogodnień, należy zapoznać się z przestrzenią nazw System.Threading.Tasks. To w niej znajdują się klasy wspierające programowanie wielowątkowe.

Zacznijmy od klasy Parallel. Jest to statyczna klasa zawierająca 3 interesujące nas metody: For, ForEach oraz Invoke (oczywiście każda metoda zawiera po kilka przeładowań, zwierających różne parametry, typy, dodatkowe opcje).

For
Jest to pierwsza a zarazem jedna z najczęściej wykorzystywanych konstrukcji w nowej bibliotece. Spójrzmy na poniższy kod:

            //Standardowa pętla for
            for (int i = 0; i < 100; i++)
            {
                Console.Write(i + "  ");
            }

            //Pętla For z biblioteki TPL
            Parallel.For(0, 100, (i) =>
                {
                    Console.WriteLine(i + "  ");
                });

Jak widać dużych różnic nie ma. Po uruchomieniu programu zobaczymy:


Wywnioskować z tego możemy, że pętla zrównoleglona nie wykonuje zadań w kolejności. Należy więc to uwzględnić przy pisaniu swojego oprogramowania.

Zobaczmy teraz realny przykład, gdzie będzie można pokazać, że wykorzystując nową pętlę przyśpieszymy wykonywane operacje. Najprostszym przykładem, a zarazem najczęściej prezentowanym jest mnożenie macierzy. Zobaczmy na kod:

        public static void MMLinear(int[,] a, int[,] b, int[,] c, int size)
        {
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            }
        }

Jak widać zwykłe linearne mnożenie. Teraz zobaczmy na wersję korzystającą z dobrodziejstw funkcji For:

        public static void MMTPL(int[,] a, int[,] b, int[,] c, int size)
        {
            Parallel.For(0, size, (i) =>
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            });
        }

Zmian jak widać nie ma wielkich poza pierwszą linijką, gdzie zamiast zwykłej pętli for mamy wykorzystaną funkcję For.
Kod samej procedury testowej wyglądał następująco:

            int size = 1000;
            int[,] a = new int[size, size];
            int[,] b = new int[size, size];
            int[,] c1 = new int[size, size];
            int[,] c2 = new int[size, size];

            Random r = new Random();
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    a[i, j] = r.Next();
                    b[i, j] = r.Next();
                }
            }

            Stopwatch s = new Stopwatch();
            s.Start();
            MatrixMultiplication.MMLinear(a, b, c1, size);
            s.Stop();
            Console.WriteLine(s.ElapsedTicks);
            Console.WriteLine();
            Thread.Sleep(2000);
            s.Restart();
            s.Start();
            MatrixMultiplication.MMTPL(a, b, c2, size);
            s.Stop();
            Console.WriteLine(s.ElapsedTicks);

Do mierzenia czasu korzystam tutaj z klasy Stopwatch. Gdy uruchomimy program zobaczymy następujące rezultaty:


Pierwszy wynik to mnożenie wykonane w zwykłej liniowej wersji funkcji. Druga wersja korzysta z zalet biblioteki TPL. Jak widać czas wykonywania operacji znacząco się skrócił (Intel Core T6400). Nie jest to 100% przyrost, ale śmiało można powiedzieć, że przy maszynach wyposażonych w większą ilość rdzeni przyrost szybkości wykonywania mnożenia macierzy będzie rósł. Przyrost szybkości można przeważnie liczyć na około 70 – 80%, co i tak jest niezłym wynikiem biorąc pod uwagę czas poświęcony na zastosowanie tego sposobu.
Spójrzmy jeszcze na deklarację Parallel.For:
For(Int32, Int32, Action)
Jest to najprostsza z konstrukcji. Podczas działania wykorzysta wszystkie dostępne rdzenie w naszym komputerze. Jeżeli chcielibyśmy mieć możliwość sami ustalić na ilu maksymalnie procesorach odbędą się obliczenia naszego zadania, możemy skorzystać z bardziej rozbudowanej konstrukcji:
For(Int32, Int32, ParallelOptions, Action)
Dla przykładu, dla naszego przykładu z macierzami:


Parallel.For(0, size, new ParallelOptions { MaxDegreeOfParallelism = 2 }, (i) =>
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            });

Wszystkie przeładowane wersje funkcji For zwracają rezultat jako obiekt typu ParallelLoopResult. Wiadomości zawarte w tym obiekcie mogą się przydać w przypadku wystąpienia wyjątku. Można wtedy sprawdzić, przy której iteracji doszło do wystąpienia wyjątku (dokładniej mówiąc jest to dolna granica wystąpienia przerwania – co to oznacza? Dla przykładu jeżeli mieliśmy 1000 iteracji a przerwanie nastąpiło przy 100 to iteracje 101 w górę nie powinny się wykonać ale te od 0 - 100 mogą nadal się wykonywać).


ForEach
Funkcja ForEach jest wielowątkowym odpowiednikiem pętli foreach:


            foreach (var item in collection)
            {

            }

            Parallel.ForEach(collection, Action);

Zobaczmy na deklarację tej metody:
ForEach(IEnumerable, Action)

Tak więc możemy śmiało posługiwać się funkcją w odniesieniu do wszystkich kolekcji implementujących interfejs IEnumerable. Przykład:

            List<int> lista = new List<int>();
            Random r = new Random();
            for (int i = 0; i < 50; i++)
            {
                lista.Add(r.Next(1000));
            }

            Parallel.ForEach(lista, (item) =>
                {
                    Console.WriteLine(item);
                });


Invoke
Ostatnia metoda zawarta w klasie Parallel to Invoke. Jak sama nazwa mówi, pozwala ona uruchomić funkcję lub tablicę funkcji. Deklaracja:
Invoke(Action[])

Przykład:


            List<Action> lista = new List<Action>();
            lista.Add(() => { Console.WriteLine("Task 1"); });
            lista.Add(() => { Console.WriteLine("Task 2"); });
            lista.Add(() => { Console.WriteLine("Task 3"); });
            lista.Add(() => { Console.WriteLine("Task 4"); });

            Array array = lista.ToArray();

            Parallel.Invoke((Action[])array);


Kilka słów odnośnie wychodzenia z pętli. Każdy wie, że z każdej pętli można wyjść wcześniej dzięki słowu kluczowemu break. Parallel.For i ForEach także mają taką możliwość. Do wykorzystania mamy dwie metody Stop i Break. Różnią się one tym, że Stop powiadamia o braku konieczności wykonywania następnych iteracji, natomiast Break gwarantuje że po aktualnej iteracji nie zostaną wykonane następne. Aby mieć możliwość korzystania omówionych funkcji należy przesłać do delegaty obiekt klasy ParallelLoopState:


            int x = 0;
            Parallel.For(0, 100, (int i, ParallelLoopState loop) =>
                {
                    x += 23;
                    if (x > 100)
                    {
                        loop.Stop();
                    }
                    Console.WriteLine(i);
                    Console.WriteLine(x);
                    Console.WriteLine("--------------------");
                });

Tyle jeżeli chodzi o pętle wielowątkowe. W następnej części trochę o klasie Task.

Brak komentarzy:

Prześlij komentarz