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, ActionJest 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
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