niedziela, 25 kwietnia 2010

Task Parallel Library cz. 1

Początkowo myślałem, aby tekst ten dodać jako trzecia część z cyklu programowanie wielowątkowe. Z drugiej strony TPL jest na tyle ciekawą co niezwykle bogatą biblioteką. Myślę, że należy poświęcić jej troszkę więcej miejsca niż tylko skwarkowy opis metody ForEach.

Programowanie wielowątkowe nigdy nie należało do łatwych. Wielu deweloperów unika stosowania go ze względu na trudności w debugowaniu aplikacji czy też trudne do wykrycia błędy powodowane przez źle zaprojektowane wątki.
Microsoft postanowił wyjść naprzeciw tym problemom i stworzył bibliotekę o nazwie Parallel Extensions. Pierwsza jej wersja została przedstawiona w listopadzie 2007 r. podczas CTP (Community Technology Preview). Następne wydania w grudniu 2007, styczniu 2008 potwierdziły tylko chęć Microsoft Research w dążeniu do stworzenia czegoś, co naprawdę ułatwi pracę programistom. Stworzona biblioteka nabiera znaczenia dopiero teraz. Została ona oficjalnie włączona do .NET 4.0 a z jej możliwości możemy korzystać w Visual Studio 2010 bez konieczności instalowania dodatkowych bibliotek. Wraz z nową biblioteką dostajemy nowe narzędzia ułatwiające debugowanie aplikacji wielowątkowych (np. Concurrency Visualizer). Całość biblioteki ładnie udokumentowano na MSDN oraz zamieszczono wiele przykładów użycia biblioteki.

Oprócz problemów dotyczących trudności w debugowaniu i projektowaniu aplikacji wielowątkowych dochodzi problem narzutu jaki niosą ze sobą wątki. Podczas tworzenia wątku na platformie .NET 4.0 rezerwowane jest 1 MB pamięci dla stosu, niezależnie od wielkości przekazywanych parametrów. Innym problemem jest ilość tworzonych wątków. Należy sobie zdać sprawę, że utworzenie 1000 wątków na maszynie wyposażonej w 4 rdzenie nie zwiększy szybkości wykonywania operacji a jedynie spowoduje większe zużycie zasobów. Dojdzie także problem przełączania między wykonującymi się wątki. W ogólności przy założeniu, że mamy 4 rdzenie i 1000 wątków może dojść do sytuacji kiedy nasze zadanie wykona się wolniej niż przy korzystaniu z aplikacji sekwencyjnej. Najlepszą wydajność uzyskujemy, gdy dla jednego rdzenia wykonuje się jeden wątek.

W .NET ilość logicznych procesorów można uzyskać za pomocą kodu:

            Environment.ProcessorCount;

Wykorzystując tą informację, możemy w logiczny sposób podzielić pracę pomiędzy wątkami. Pojawia się jednak mały problem tutaj. Wyobraźmy sobie tablicę zadań. Powiedzmy że mamy 20 zadań. Pierwsze zadanie zajmuje 1 sek., drugie 2 sek., trzecie 3 sek., itd. Całkowity czas wykonania wszystkich zadań to 210 sek. Pierwsze co nam rzuca się na myśl to podzielenie tablicy zadań na dwie równe. Pierwszy wątek mógłby wykonywać zadania od 1 do 10 a drugi od 11 do 20. Korzystając z takiego rozwiązania otrzymujemy jednak wersję w której pierwszy wątek wykona swoje zadania po 55 sek. a drugi po 155 sek. Podział więc nie jest za dobry. Kolejnym rozwiązaniem mógłby być podział naprzemienny:


Przy takim założeniu, 1 wątek będzie pracować przez 100 sek., gdy tymczasem drugi 110. Podział jest dużo lepszy od pierwszego pomysłu.

Jak w .NET zrealizowany jest ten problem?
Zobaczmy na diagram:


Jak widać na diagramie istnieje jedna główna kolejka zadań. Z tej kolejki zadania następnie przesyłane są na mniejsze kolejki zadań dla każdego z procesorów. Kolejki te mogą miedzy sobą wymieniać się informacjami. Dla przykładu jeżeli Core 1 (procesor/rdzeń 1) wykonał już wszystkie zadania które miał w swojej kolejce, może poszukać zadań do realizacji u sąsiada (Core 2). Można teraz domyślić się, że wykonywane zadania mogą nie odbywać się w takiej kolejności w jakiej chcielibyśmy. Przypuszczając że mamy 400 zadań i 4 rdzenie nie mamy pewności, że każdy z rdzeni wykona 100 zdań. Należy więc mieć na uwadze gdy projektujemy metody, które wykorzystamy dla TPL. Kolejnym aspektem jest wykrywanie wyjątków. Należy sobie zdać pytanie: co w momencie gdy dojdzie do wyjątku? TPL zostało tak zaprojektowane, że gdy dojdzie do wystąpienia wyjątku podczas wykonywania zadania na jednym z rdzeni, TPL wyrzuci wyjątkiem i anuluje zadania. TPL nie gwarantuje, że w momencie wyrzucenia wyjątku przestanie wykonywać zadania. Komunikacja pomiędzy kolejkami dla zadań jest tutaj kluczowym aspektem. Co w przypadku gdy na wszystkich rdzeniach wystąpi wyjątek? Zostanie rzucony inny wyjątek uwzględniający tę sytuację.

Brak komentarzy:

Prześlij komentarz