Optymalizacja aplikacji użytkowych z wykorzystaniem Parallel Extensions



Podobne dokumenty
Współbieżność i równoległość w środowiskach obiektowych. Krzysztof Banaś Obliczenia równoległe 1

SZYBKO ZROZUMIEĆ VISUAL BASIC 2012 Artur Niewiarowski -

Aplikacje w Javie- wykład 11 Wątki-podstawy

Autor: dr inż. Zofia Kruczkiewicz, Programowanie aplikacji internetowych 1

Wątek - definicja. Wykorzystanie kilku rdzeni procesora jednocześnie Zrównoleglenie obliczeń Jednoczesna obsługa ekranu i procesu obliczeniowego

4. Procesy pojęcia podstawowe

Stworzenie klasy nie jest równoznaczne z wykorzystaniem wielowątkowości. Uzyskuje się ją dopiero poprzez inicjalizację wątku.

Java. Wykład. Dariusz Wardowski, Katedra Analizy Nieliniowej, WMiI UŁ

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

Podczas dziedziczenia obiekt klasy pochodnej może być wskazywany przez wskaźnik typu klasy bazowej.

Obiekt klasy jest definiowany poprzez jej składniki. Składnikami są różne zmienne oraz funkcje. Składniki opisują rzeczywisty stan obiektu.

Wielowątkowość mgr Tomasz Xięski, Instytut Informatyki, Uniwersytet Śląski Katowice, 2011

Programowanie obiektowe

4. Procesy pojęcia podstawowe

Programowanie wielowątkowe. Tomasz Borzyszkowski

Programowanie obiektowe

Programowanie obiektowe

Język Java wątki (streszczenie)

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Wykład 4 Delegat (delegate), właściwości indeksowane, zdarzenie (event) Zofia Kruczkiewicz

Wyjątki. Streszczenie Celem wykładu jest omówienie tematyki wyjątków w Javie. Czas wykładu 45 minut.

Java. język programowania obiektowego. Programowanie w językach wysokiego poziomu. mgr inż. Anna Wawszczak

Laboratorium z przedmiotu Programowanie obiektowe - zestaw 04

Współbieżność w środowisku Java

msgbox("akcja: Początek, argument: " + argument.tostring()); Thread.Sleep(1000); //opóźnienie msgbox("akcja: Koniec"); return DateTime.Now.

C# 6.0 : kompletny przewodnik dla praktyków / Mark Michaelis, Eric Lippert. Gliwice, cop Spis treści

LINQ TO XML. Autor ćwiczenia: Marcin Wolicki

Podstawy i języki programowania

Czym jest Java? Rozumiana jako środowisko do uruchamiania programów Platforma software owa

1 Wątki 1. 2 Tworzenie wątków 1. 3 Synchronizacja 3. 4 Dodatki 3. 5 Algorytmy sortowania 4

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Podstawy programowania. Wprowadzenie

Instrukcja laboratoryjna cz.3

Programowanie obiektowe. Literatura: Autor: dr inŝ. Zofia Kruczkiewicz

Programowanie wielowątkowe. Jarosław Kuchta

Temat: Ułatwienia wynikające z zastosowania Frameworku CakePHP podczas budowania stron internetowych

Programowanie komputerów

Konstruktory. Streszczenie Celem wykładu jest zaprezentowanie konstruktorów w Javie, syntaktyki oraz zalet ich stosowania. Czas wykładu 45 minut.

SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE

Programowanie obiektowe

Jeśli chcesz łatwo i szybko opanować podstawy C++, sięgnij po tę książkę.

SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE

Obliczenia równoległe i rozproszone w JAVIE. Michał Kozłowski 30 listopada 2003

Programowanie w języku Java - Wyjątki, obsługa wyjątków, generowanie wyjątków

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

Java jako język programowania

Języki i paradygmaty programowania - 1

Programowanie obiektowe

Microsoft IT Academy kurs programowania

REFERAT PRACY DYPLOMOWEJ

1 Podstawy c++ w pigułce.

Wprowadzenie do języka Java

Rozdział 4 KLASY, OBIEKTY, METODY

Języki i paradygmaty programowania doc. dr inż. Tadeusz Jeleniewski

Pętle. Dodał Administrator niedziela, 14 marzec :27

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

JAVA. Java jest wszechstronnym językiem programowania, zorientowanym. apletów oraz samodzielnych aplikacji.

Programowanie obiektowe zastosowanie języka Java SE

Wprowadzenie do projektu QualitySpy

Podstawy Programowania 2

Task Parallel Library

Programowanie obiektowe

Rozdział 3. Zapisywanie stanu aplikacji w ustawieniach lokalnych

Multimedia JAVA. Historia

Java: otwórz okienko. Programowanie w językach wysokiego poziomu. mgr inż. Anna Wawszczak

WPROWADZENIE DO JĘZYKA JAVA

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Czym są właściwości. Poprawne projektowanie klas

Zasady programowania Dokumentacja

Być może jesteś doświadczonym programistą, biegle programujesz w Javie,

Zaawansowane aplikacje WWW - laboratorium

Podręcznik użytkownika Obieg dokumentów

JAVA W SUPER EXPRESOWEJ PIGUŁCE

Expo Composer Garncarska Szczecin tel.: info@doittechnology.pl. Dokumentacja użytkownika

Programowanie obiektowe i zdarzeniowe wykład 4 Kompozycja, kolekcje, wiązanie danych

Spis treści. 1 Java T M

Rozdział 5. Administracja kontami użytkowników

using System;... using System.Threading;

Programowanie Strukturalne i Obiektowe Słownik podstawowych pojęć 1 z 5 Opracował Jan T. Biernat

Programowanie współbieżne i rozproszone

Java EE produkcja oprogramowania

REFERAT O PRACY DYPLOMOWEJ

Obszar statyczny dane dostępne w dowolnym momencie podczas pracy programu (wprowadzone słowem kluczowym static),

Podstawy programowania skrót z wykładów:

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Tworzenie prezentacji w MS PowerPoint

5.2. Pierwsze kroki z bazami danych

Tutorial prowadzi przez kolejne etapy tworzenia projektu począwszy od zdefiniowania przypadków użycia, a skończywszy na konfiguracji i uruchomieniu.

Lab 9 Podstawy Programowania

Zakres tematyczny dotyczący podstaw programowania Microsoft Office Excel za pomocą VBA

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Języki i techniki programowania Ćwiczenia 2

Programowanie MorphX Ax

Ćwiczenie 1. Przygotowanie środowiska JAVA

Rys. 1. Główne okno programu QT Creator. Na rysunku 2 oznaczone zostały cztery przyciski, odpowiadają kolejno następującym funkcjom:

Programowanie zaawansowane

Backend Administratora

Kurs programowania. Wykład 8. Wojciech Macyna

Kurs programowania. Wykład 8. Wojciech Macyna. 10 maj 2017

Klasy Obiekty Dziedziczenie i zaawansowane cechy Objective-C

Transkrypt:

Politechnika Łódzka Wydział Fizyki Technicznej, Informatyki i Matematyki Stosowanej Marcin Biegała Optymalizacja aplikacji użytkowych z wykorzystaniem Parallel Extensions (Optimizing software applications using Parallel Extensions) Praca dyplomowa magisterska (inżynierska) Promotor: dr inż. Dariusz Puchała Dyplomant: Marcin Biegała nr albumu 133724 Łódź, wrzesień 2010

Spis treści 1 Wstęp 5 Cel pracy............................................. 6 Konwencje............................................. 7 Podziękowania.......................................... 7 I Część teoretyczna 9 2 Wielowątkowość 11 2.1 Słowo wstępu........................................ 11 2.2 System operacyjny..................................... 11 3.NET Framework 13 3.1 Notka historyczna..................................... 13 3.2 Czym jest.net Framework?............................... 14 3.3 Wersje.NET Framework.................................. 15 3.4 MSIL............................................ 15 3.5 System.Threading..................................... 16 3.5.1 Klasa Thread.................................... 16 3.5.2 Współdzielenie danych............................... 20 3.5.3 Klasa ThreadPool................................. 25 3.6 Parallel Extensions..................................... 28 3.6.1 Klasa Task..................................... 29 3.6.2 Współdzielenie danych............................... 33 3.6.3 Dodatki....................................... 34 3.6.4 Planista - Task Scheduler............................. 37 II Część praktyczna 39 4 Parallel Image Effects 41 4.1 Opis aplikacji........................................ 41 4.2 Funkcjonalności....................................... 42 4.3 Budowa projektu...................................... 48 4.3.1 Technologie i narzędzia.............................. 48

4 SPIS TREŚCI 4.3.2 Budowa projektu.................................. 49 4.3.3 Diagram UML................................... 50 4.3.4 Szczegóły implementacji.............................. 51 5 Testy 55 5.1 Operacje na drzewie binarnym.............................. 55 5.2 Zbiory Mandelbrota.................................... 59 5.3 Filtry graficzne....................................... 66 Bibliografia 71 Spis rysunków 73 Spis tabel 75 Listingi 78

Rozdział 1 Wstęp Stwierdzenie, że komputery są obecne w każdym aspekcie naszego życia nie jest niczym odkrywczym. Poza komputerami, procesory, lub pokrewne układy elektroniczne, pełniące funkcję centralnych jednostek obliczeniowych odnajdziemy chociażby w telefonach komórkowych, sprzęcie AGD/RTV, czy samochodach. Wraz z zajmowaniem kolejnych gałęzi przemysłu i życia codziennego, rośnie moc obliczeniowa jednostek sterujących. Powszechnie znane (choć odrobinę przejaskrawione) jest powiedzenie, iż komputer w chwili zakupu jest już przestarzały. W 1965 roku Gordon Moore 1 na podstawie obserwacji sformułował tezę, zwaną dziś Prawem Moora[2], która określa, że liczba tranzystorów w układzie scalonym podwaja się co 12 miesięcy. W kolejnych latach, liczba ta była korygowana i obecnie przyjmuje się, że liczba tranzystorów podwaja się co 24 miesiące. Rysunek 1.1 obrazuje w/w tezę. Prawo Moora, choć powstało w latach sześćdziesiątych ubiegłego wieku, jest zadziwiająco trafne do dnia dzisiejszego. Jednak dziś, wiemy już, że prawo to przestanie obowiązywać w przeciągu 2-3 lat. Wzrastająca liczba tranzystorów implikuje ich coraz mniejszy rozmiar. Obecnie dominującą technologią jest 45nm, podczas gdy w latach dziewięćdziesiątych ubiegłego wieku procesory tworzono w technologii 500nm. Malejący rozmiar tranzystora, prowadzi do oczywistej konkluzji: aby Prawo Moora wciąż obowiązywało, w niedługim czasie rozmiar tranzystora powinien być mniejszy od rozmiarów atomu. Docieramy zatem do fizycznej granicy mocy obliczeniowaj procesora. Skoro wydajność jednego procesora jest już nie wystarczająca, oczywistym jest próba wykorzystania dwóch i więcej jednostek obliczeniowych. Tak powstał pomysł procesorów wielordzeniowych - w jednej obudowie zamknięto wiele rdzeni odpowiedzialnych za obliczenia. Bardzo ważnym i wymagającym podkreślenia jest fakt, iż procesor dwurdzeniowy nie jest dwa razy szybszy od swojego jednordzeniowego odpowiednika o takim samym taktowaniu. Jego przewagą jest możlwiość wykonania dwóch operacji jednocześnie, dokładnie w tej samej chwili. Bardzo do- 1 Gordon Earle Moore (ur. 3 stycznia 1929), współzałożyciel korporacji Intel.[1]

6 Rozdział 1. Wstęp brym porównaniem może poszczycić się tu Dan Reed 2 opisując różnicę między procesorem jedno i wielordzeniowym: Różnica jest taka jak między szybkim sportowym autem, a autobusem szkolnym. Pierwszy szybko przewiezie dwie osoby, a drugi, choć trochę wolniej czterdzieści. Rysunek 1.1: Ilustracja prawa Moora[2] Cel pracy Praca ta powstała w odpowiedzi na obecnie panujące trendy w dziedzinie inżynierii oprogramowania. Zdecydowana większość obecnie produkowanych procesorów, wytwarzana jest w technologii wielordzeniowej. Mimo to, duża część powstającego oprogramowanie nie potrafi wykorzystać pełni ich potencjału. Zauważył to przytaczany już Dan Reed: Już niedługo zabraknie programistów z doświadczeniem w tworzeniu aplikacji wykorzystujących przetwarzanie równoległe. To już ostatni dzwonek, aby przekonać młodych programistów o wartości przetwarzania równoległego. Celem pracy jest analiza przydatności nowego rozwiązania zapropowanego przez Microsoft - Parallel Extension i porównanie go z dotychczas dostępnymi narzędziami z przestrzeni nazw System.Threading. Porównanie zostanie przeprowadzone zarówno pod kątem możliwości, wydajności jak również efektywności, co rozumiem przez ilość pracy włożonej by uzyskać satysfakcjonujący efekt. 2 Dan Reed - wiceprezes działu Extreme Computing w firmie Microsoft[3]

7 Pracy nie kieruję do grona programistów, którzy na co dzień zajmują się problemami przetwarzania równoległego, a do szerokiego spektrum programistów, którzy dzień po dniu tworzą rozwiązania informatyczne w oparciu o platformę.net Framework. Mam nadzieję, że w przedstawionym materiale uda mi się zawrzeć kilka prostych zabiegów, które pozwolą tworzonym aplikacjom wykorzystać całą moc drzemiącą w procesorach wielordzeniowych. Podziękowania Z tego miejsca chciałbym złożyć gorące podziękowania na ręce promotora niniejszej pracy, dr Dariusza Puchały, za poświęcony czas, cierpliwość i cenne wskazówki. Chciałbym także podziękować panu Tomaszowi Kopaczowi z firmy Microsoft Polska, za użyczenie materiałów wykorzystywanych w jego prezentacjach na temat Parallel Extensions.

8 Rozdział 1. Wstęp

Część I Część teoretyczna

Rozdział 2 Wielowątkowość 2.1 Słowo wstępu W pierwszych słowach tego rozdziału chciałbym wyjaśnić pewną kwestię nazewniczą. Problem tkwi w określeniach programowanie, czy też wykonywanie współbieżne i równoległe. Mimo, z pozoru podobieństwa znaczeniowego, nie można w/w wyrażeń stosować zamiennie. Wykonywanie współbieżne dotyczy przetwarzania instrukcji aplikacji przez jeden procesor. Kolejne wątki, zarządzane przez system operacyjny wykonywane są naprzemiennie. Zmiany wykonywanych wątków dokonywane są tak często i szybko, że sprawiają wrażenie jednoczesnego wykonywania operacji. Praca równoległa dotyczy systemów wyposażonych w więcej niż jeden procesor (bądź rdzeń), a instrukcje, w kolejnych wątkach wykonywane są dokładnie w tym samym momencie, na oddzielnych jednostkach obliczeniowych. 2.2 System operacyjny Choć wykonanie kolejnych instrukcji podzielonych na wątki jest zadaniem procesora, programista z reguły korzysta w tej kwestii z funkcji udostępnianych przez system operacyjny. To on stanowi warstwę pośredniczącą, pomiędzy kodem aplikacji, a centralną jednostką obliczeniową. To system dba o przełączanie procesów, przydzielanie odpowiednich obszarów pamięci, pozwalając programiście skupić się jedynie na działaniu jego aplikacji. Cechą jaką powinien wyróżniać się system operacyjny jest wielozadaniowość, czyli zdolność do uruchomienia i obsłużenia wielu aplikacji jednocześnie, co pozwala użytkownikowi edytować dokument, podczas gdy w tle kopiowane są pliki, a z głośników sączy się przyjemna muzyka. Początki komputerów PC to procesory z rodziny Intel 8088, jednak nie zostały ona zaprojektowane pod kątem wielozadaniowości. Dużym problemem była obsługa przenoszenia obszarów pamięci, tak aby zagospodarować jak najwięcej wolnej przestrzeni podczas uruchamiania i zamykania aplikacji. Kolejnym krokiem w historii komputerów osobistych było wprowadzenie do użytku sytemu operacyjnego DOS (Disk Operating System). Choć nie wprowadzał on funkcjonalności dla aplikacji wielowątkowych, dzięki różnego rodzaju trikom, możliwe było stworzenie programu, który dawał

12 Rozdział 2. Wielowątkowość użytkownikowi złudzenie działania współbieżnego. Najpopularniejszym podejściem było wykorzystanie sprzętowego licznika przerwać. Aplikacje tego typu określano mianem TSR (terminate-andstay-resident). DOS zyskał ogromną popularność, co zaowocowało wieloma aplikacjami będącymi nakładkami na prosty interfejs systemu i rozszerzającymi jego funkcjonalność. Należały do nich między innymi systemy z rodziny Microsoft Windows. Windows 1.0 był na tyle rozwiniętą aplikacją(w porównaniu do DOSa, na którym się opierał), że potrafił przemieszczać bloki danych w pamięci operacyjnej (co jak zostało wcześniej nadmienione, jest warunkiem do zapewnienia obsługi wielozadaniowości). Choć obsługa nie była w pełni transparentna dla programisty, pozwalała na wykorzystywanie udogodnień oferowanych przez istniejące już systemy wielozadaniowe, jak np. UNIX. Sama obsługa wielu aplikacji w Windows była zupełnie odmienna, niż ta prezentowana w UNIXie. Systemy z rodziny UNIX implementowały wielozadaniowość z wywłaszczaniem (preemptive multitasking), gdzie moc obliczeniowa była dysponowana pomiędzy aplikacjami na podstawie sygnałów z licznika procesora. W Windows zastosowano mechanizm wielozadaniowości bez wywłaszczania, nie operujący czasem procesora, a korzystający z systemu komunikatów, krążących pomiędzy aplikacjami. Dany program obsługiwał przekazany doń komunikat (co z reguły skutkowało pewną operacją widoczną dla użytkownika), po czym kontrola wracała do systemu. Stąd, ten typ obsługi wielozadaniowości zwany był wielozadaniowością kooperacyjną, gdyż wymagał od autorów aplikacji uwzględnienia równoczesnego działania innych programów w systemie.

Rozdział 3.NET Framework 3.1 Notka historyczna Wiele plotek głosi, że Microsoft tworząc.net Framework po prostu przepisał Javę tworzoną przez firmę SUN. I choć w samym frameworku można dostrzec wiele analogii do Javy, to jednak jego korzeni należy szukać w zupełnie innych rozwiązaniach. Podwaliną dla przyszłej technologi były prace trzech niezależnych zespołów programistycznych zajmujących się: - COM 2.5 1 - ASP 4.0 2 - Next-Generation Windows Services 3 Początków.Net Framework należy szukać w opracowanym w latach dziewięćdziesiątych ubiegłego wieku języku Microsoft J++, który to miał być zgodny z oryginalną implementacją Javy firmy Sun, i rozszerzać ją o szereg funkcjonalności wymaganych przez programistów środowiska Windows, m.in. obsługę obiektów COM. Choć początkowo Sun licencjonował kolejne wersje J++, nie przeszkodziło mu to wytoczyć w 1998 roku pozwu o naruszenie patentów. To posunięcie ze strony Sun, było impulsem do stworzenia.net Framework. Microsoft zebrał dokonania wokół nowej wersji interfejsu COM, ASP oraz kod maszyny wirtualnej OmniVM 4, by 13-tego lutego 2002 roku światło dzienne ujrzała nowa platforma programistyczna. 1 Component Object Model - opracowany przez Microsoft interfejs pozwalający na towrzenie i interakcję obiektów niezależnie od języka programowania 2 Active Server Pages - technologia Microsoft służąca do tworzenia dynamicznych stron WWW 3 projekt znany był także pod nazwą Project Lightning lub Project 42, we wstępnej fazie dotyczył rozwinięcia istniejącego standardu COM 4 OmniVM była maszyną wirtualną stworzoną przez Stevena Lucco z Carnegie Mellon University. W 1994 roku Lucco założył firmę Colusa Software, która dwa lata później została wykupiona przez Microsoft, wraz z prawami do OmniVM

14 Rozdział 3..NET Framework 3.2 Czym jest.net Framework?.NET Framework jest platformą programistyczną wprowadzającą zupełnie nową jakość tworzenia aplikacji w systemach z rodziny Windows, w porównaniu do WIN32 API. Struktura.NET Framework (zilustrowana na rysunku ) składa się z następujących głównych elementów CLR(Common Language Runtime) - stanowi środowisko uruchomieniowe dla aplikacji napisanych w.net (pewnego rodzaju maszyna wirtualna). Pozwala tworzyć aplikację nie zastanawiając się nad konfiguracją sprzętową maszyny, na której aplikacja będzie uruchamiana, czy zainstalowanymi tam bibliotekami. Jednocześnie zwalnia programistę z zarządzania uprawnieniami aplikacji, czy alokowania pamięci (poprzez mechanizm odśmiecania pamięci - Garbage Collecting GC). CLR jest również pomocny w obsłudze sytuacji wyjątkowych(exceptions). CLR w.net jest środowiskiem uruchomieniowym dla wielu języków (w przeciwieństwie do Javy, gdzie JVM jest wspólnym środowiskiem uruchomieniowym dla różnych platform). Base Class Library - kolekcja zawartych w.net Framework typów i algorytmów, które wykorzystujemy podczas tworzenia aplikacji. Kolekcja jest w pełni obiektowa i pozwala w łatwy sposób rozszerzać zawarte w niej rozwiązania. Biblioteka standardowa zawiera m.in. implementację podstawowych typów, kolekcji, algorytmy kryptograficzne, czy gotowe kontrolki wykorzystywane do budowania interfejsów użytkownika. Rysunek 3.1: Struktura.NET Framework 2.0[9] Całości dopełnia środowisko programistyczne - Microsoft Visual Studio.NET 5 5 Istnieją także inne środowiska programistyczne, jak np. SharpDevelop, lecz niekwestionowanym liderem na platformie Windows jest Visual Studio i to z jego pomocą tworzony był kod towarzyszący niniejszej pracy

3.3 Wersje.NET Framework 15 3.3 Wersje.NET Framework Przez siedem lat istnienia.net Framwerk na rynku, pojawiło się wiele kolejnych wersji. Podsumowanie najważniejszych zmian w kolejnych wersjach[10] zostało zebrane w tabeli 3.1. Tabela 3.1: Wersje.NET Framework Wersja Data wydania Opis 1.0 2002-02-13 Pierwsza wersja.net Framework 1.1 2003-04-24 Zespół poprawek do wersji 1.0 m.in. roszerzona obsługa zabezpieczeń w aplikacjach WinForms, obsługa IPv6, wprowadzenie.net Framework CF (Compact Framework). Pierwsza wersja frameworka zintegrowana z systemem operacyjnym(windows Server 2003) 2.0 2005-11-07 Wprowadzenie klas generycznych, typów nullowalnych (nullable types), klas partial, metod anonimowych, rozszrzenie kontrolek ASP.NET 3.0 2006-11-06 Wprowadzenie Windows Presentation Foundation(WPF), Windows Comunication Foundation(WCF), Windows Workflow Foundation(WF). Integracja z systemami Windows Vista i Windows Server 2008 3.5 2007-11-19 Dodanie wyrażeń lambda, extension methods, obsługi AJAX do ASP.NET, LINQ 4.0 2010-04-12 Wprowadzenie Parallel Extension, słowo kluczowe dynamic, pełna obsługa IronPython, IronRuby, F# Od wersji 2.0 Microsoft nie wproawdzał znaczących zmian do kodu maszyny uruchomieniowej, a wersja ta stała się trzonem kolejnych wydań.net Framework. 3.4 MSIL Aplikacje napisane w oparciu o.net Framework kompilowane są do języka pośredniego(tzw. bytecode), który nazwano Microsoft Intermediate Language - MSIL. MSIL jest językiem składającym się z niezależnych od procesora instrukcji, które łatwo mogę zostać przekształcone do kodu natywnego. W składni MSIL przypomina assemblera. Listing 3.1 przedstawia kod pośredni wygenerowany dla prostej aplikacji typu Hello World 6.. method private hidebysig static void Main ( string [] args ) cil managed. entrypoint. maxstack 8 L_0000 : ldstr " Hello World " L_0005 : call void [ mscorlib ] System. Console :: WriteLine ( string ) L_000a : ret 6 Kod otrzymany za pomocą narzędzia Reflector firmy Red Gate http://www.red-gate.com/products/reflector/

16 Rozdział 3..NET Framework Listing 3.1: Przykład kodu MSIL 3.5 System.Threading Choć będące tematem niniejszej pracy Parallel Extension stało się częścią.net Framework dopiero od wersji 4.0, framework udestępniał wcześniej narzędzia do tworzenia aplikacji wielowątkowych - przestrzeń System.Threading. Postaram się teraz pokrótce przedstawić pracę z klasami w niej zawartymi i możliwości jakie oferują. 3.5.1 Klasa Thread Głównym elementem przestrzeni System.Threading jest klasa Thread. Pozwala ona tworzyć i zarządzać kolejnymi wątkami. Obiekty klasy Thread odzwierciedlają pojedyncze wątki. Tabele 3.2 i 3.3 prezentują podstawowe właściwości i metody charakteryzujące te obiekty. Tabela 3.2: Podstawowe właściwości obiektów klasy Thread Nazwa Opis IsAlive Określa, czy dany wątek jest aktualnie wykonywany IsBackground Określa, wątek wykonywany jest w tle (z niższym priorytetem) IsThreadPoolThread Określa czy wątek należy do ThreadPool ManagedThreadId Indeks przydzielany wątkowi przez CLR Name Opcjonalna nazwa wątku Priority Priorytet z jakim wykonywany jest wątek ThreadState Określa stan danego wątku Nazwa Abort Interrupt Join Start Tabela 3.3: Podstawowe metody obiektów klasy Thread Opis Wysyła do wątku żądanie przerwanoa Zgłasza ThreadInterruptException, jeżeli wątek jest zablokowany Blokuje wywoływany wątek, póki inny wątek nie zakończy działania Ustawia wątek w kolejce do wykonania Każdy wątek określony jest enumeracją ThreadState, której kolejne elementy przedstawia tabela 3.4. Aby w.net Framework wykonać zadany kod w wątku innym niż główny, musimy wykonać dwa kroki. Pierwszym jest stworzenie metody, zawierajacej kod do wykonania. Listing 3.2 zawiera przykładowy kod takiej metody. Wykorzystano w nim statyczną właściwość CurrentThread, klasy Thread

3.5 System.Threading 17 Tabela 3.4: Wartości enumeracji ThreadState Nazwa Opis Aborted Wykonanie wątku zostało przerwane AbortRequested Żądanie przerwania zostało wysłane, lecz sam wątek wciąż pracuje Background Wątek pracuje w tle Running Wątek jest uruchomiony Stopped Wykonanie wątku zostało zatrzymane StopRequested Żądanie zatrzymania wątku zostało wysłane Suspended Wykonanie wątku zostało zawieszone 7 SuspendRequested Żądanie zawieszenia wątku zostało wysłane Unstarted Wątek został stworzony, lecz nie został jeszcze uruchomiony WaitSleepJoin Wątek jest zablokowany w oczekiwaniu na Monitor.Wait, Thread.Sleep lub Thread.Join zawierającą wskazanie na aktualnie wykonywany wątek. Drugim krokiem jest stworzenie obiektu ThreadStart opakowującego naszą metodę i samego obiektu Thread reprezentującego nasz wątek. Listing 3.3 przedstawia przykładowy kod. private void DoWork () // wypisujemy w konsoli krótki tekst, wraz z identyfikatorem wątku nadanym // przez CLR Console. WriteLine (" Ciężko pracuje. Wątek : 0 ", Thread. CurrentThread. ManagedThreadId ); Listing 3.2: Przykładowy kod metody wykonywanej w wątku pobocznym ThreadStart methodref = new ThreadStart ( DoWork ); \\ parametrem jest nazwa metody, zawierającej \\ kod do wykonania w wątku Thread thread = new Thread ( methodref ); \\ tworzymy wątek thread. Start (); \\ uruchamiamy wątek Listing 3.3: Przykładowy kod tworzący wątek Po uruchomieniu takiego kodu, naszym oczom powinien ukazać się napis Ciężko pracuje. Wątek: 3. Nic nie stoi na przeszkodzie, by rozwinąć nasz przykład i stworzyć np 10 nowych wątków. Wymagane zmiany przedstawia listinig 3.4. ThreadStart methodref = new ThreadStart ( DoWork ); \\ parametrem jest nazwa metody, zawierającej \\ kod do wykonania w wątku for ( int i =0; i <10;++ i)

18 Rozdział 3..NET Framework Thread thread = new Thread ( methodref ); \\ tworzymy wątek thread. Start (); \\ uruchamiamy wątek Listing 3.4: Tworzenie i uruchamianie wielu wątków Po uruchomieniu powinniśmy otrzymać mniej więcej taki rezultat: Ciężko pracuje. Wątek: 3 Ciężko pracuje. Wątek: 4 Ciężko pracuje. Wątek: 5 Ciężko pracuje. Wątek: 6 Ciężko pracuje. Wątek: 7 Ciężko pracuje. Wątek: 8 Ciężko pracuje. Wątek: 9 Ciężko pracuje. Wątek: 10 Ciężko pracuje. Wątek: 11 Ciężko pracuje. Wątek: 12.NET Framework umożliwia także uruchamianie wątków z zadanym parametrem. Przedstawia to listing 3.5. private void DoWork ( object param ) // wypisujemy w konsoli krótki tekst, wraz z identyfikatorem wątku nadanym // przez CLR Console. WriteLine (" Ciężko pracuje. Wątek : 0. Parametr : 1 ", Thread. CurrentThread. ManagedThreadId, ( param is String )? ( string ) param : ""); public void Main () ParameterizedThreadStart methodref = new ParametrizedThreadStart ( DoWork ); Thread thread = new Thread ( methodref ); \\ tworzymy wątek thread. Start (" Ala ma kota "); \\ uruchamiamy wątek Listing 3.5: Uruchamianie wątku z parametrem Bardzo często wykorzystywaną funkcjonalnością podczas tworzenia aplikacji wielowątkowych jest możliwość wstrzymiania wykoniania jednego wątku, do czasu zakończenia pracy przez inny wątek. W przestrzeni System.Threading służy do tego metoda Thread.Join. Przykład wykorzystania przedstawia listing 3.6.

3.5 System.Threading 19 ThreadStart methodref = new ThreadStart ( DoWork ); \\ parametrem jest nazwa metody, zawierającej \\ kod do wykonania w wątku \\ tablica przechowująca referencje do tworzonych wątków Thread [] threads = new Thread [10]; for ( int i =0; i <10;++ i) \\ tworzymy wątek threads [ i] = new Thread ( methodref ); threads [ i]. Start (); \\ uruchamiamy wątek // wstrzynujemy główny wątek, do czasu wykonania // każdego z nowoutworzonych wątków foreach ( var thread in threads ) thread. Join (); Listing 3.6: Wykorzystanie Thread.Join System.Threading udostępnia także możliwość zatrzymania wykonywania wątku, służy do tego wspomniana wyżej metoda Thread.Abort. Jednak wykorzystanie jej jest bardziej zawiłe niż wykorzystanie przedstawionych do tej pory funkcjonalności. Wywołanie Thread.Abort skutkuje wyrzuceniem wyjątku ThreadAbortException po aktualnie wykonywanej instrukcji wątku. Takie rozwiązanie pozwala przechwycić i obsłużyć wyjątek zapewniając integralność stanu aplikacji. Wspomniany problem integralności stanu aplikacji wielowątkowej prowadzi do dwóch kolejnych metod zawartych w przestrzeni System.Threading, a mianowicie Thread.BeginCriticalRegion() i Thread.EndCriticalRegion(). Jak można wywnioskować z ich nazw, służą one do wydzielania sekcji krytycznych, czyli fragmentów kodu, którego wykonanie nie może zostać przerwane poprzez przekazanianie czasu procesora do innego wątku, czy procesu. Listing 3.7 przedstawia przykład metody zawierającej sekcję krytyczną. private void Sample () Thread. BeginCriticalRegion (); // poniższy kod nie zostanie przerwany // przez wykonywanie innego wątku bool result = PerformCalculations (); if( result == true ) this. IsValid = true ; else

20 Rozdział 3..NET Framework this. IsValid = false ; Thread. EndCriticalRegion (); Listing 3.7: Przykład sekcji krytycznej Rysunek 3.2 ilustruje zasadę działania sekcji krytycznych w aplikacjach wielowątkowych. Rysunek 3.2: Ilustracja sekcji krytycznych[15] 3.5.2 Współdzielenie danych Choć problemy współbieżnego dostępu do zasobów nie są tematem niniejszej pracy, nie można rozpatrywać wielowątkowości, nie poruszając tego zagadnienia. Ale gdzie tu problem? Za przykład posłuży nam poniższy kod: public class Program // zmienna statyczna - mamy pewność // że istnieje tylko jedna instancja private static int _ counter ; // interesująca nas akcja public static class Increment ()

3.5 System.Threading 21 for ( int i =0; i <=10000; ++i) _ counter = _ counter + 1; public static void Main () Increment (); Console. WriteLine ( _counter ); Listing 3.8: Przykładowe odwołanie do zmiennej - jeden wątek Listing 3.8 przedstawia kod, który wykonany dowolną ilość razy, zawsze zwróci ten sam wynik: na konsoli wyświetlona zostanie liczba 10000. Dokonajmy teraz drobnej modyfikacji i wprowadźmy element wielowątkowości zgodnie z listingiem 3.9. public static void Main () ThreadStart entry = new ThreadStart ( Increment ); // tworzymy i uruchamiamy 10 wątków Thread [] threads = new Thread [10]; for ( int i =0; i <10; i ++) threads [i] = new Thread ( entry ); threads [i]. Start (); // czekamy na zakończenie // każdego z wątków foreach ( var thread in threads ) thread. Join (); // tu spodziewamy się wyniku // 10* 10000 = 100000 Console. WriteLine ( _counter ); Listing 3.9: Przykładowe odwołanie do zmiennej - wiele wątków

22 Rozdział 3..NET Framework Wynik działania powyższego kodu może być zaskakujący, a na domiar złego, różny przy każdym uruchomieniu. Owszem, zdarza się, że program poda prawidłowo 100000, ale wyświetla także 99997, 99998 czy 99993. Przedstawiony kod jest jedynie sztucznym przykładem, nie mającym nic wspólnego z rzeczywistością, ale można sobie wyobrazić, gdyby podobne operacje wykonywane były w systemie bankowym, na koncie jednego z klientów. Co gorsza, z racji losowości występowania, błąd tego typu może nie zostać wychwycony w trakcie testów, a skutki mogą być katastrofalne. Pora zdefiniować źródło błędu, a jest nim poniższa linijka: _counter = _counter + 1; Listing 3.10 przedstawia kod IL wygenerowany dla tego odwołania. // przeniesienie wartości zmiennej statycznej na stos ldsfld int32 ConsoleApplication. Program :: _ counter // wartość całkowita 1 dodawana jest na stos ldc.i4.1 // wstawione wartości zostają dodane add // przeniesienie wartości ze stosu do zmiennej statcznej stsfld int32 ConsoleApplication. Program :: _ counter Listing 3.10: Kod pośredni dla operacji przypisania Jak widać prosta instrukcja inkrementacji zmiennej, rozbita zostaje na cztery kolejne instrukcje. Generalizując, przypisanie wartości do zmiennej możemy opisać w trzech krokach: 1. Pobranie wartości zmiennej 2. Inkrementacja 3. Zapisanie nowej wartości zmiennej Wiemy już, że w trakcie działania aplikacji wielowątkowych, czas procesora przełączany jest pomiędzy wykananiem instrukcji poszczególnych wątków. Może się zatem zdarzyć sytuacja, w której wykonanie dwóch wątków, zostanie przerwane po operacji pobrania wartości zmiennej. Mamy wtedy sytuację, w której oba wątki przetrzymują tą samą wartość zmiennej, którą następnie inkrementują i zapisują z powrotem. Wynikiem czego, zamiast zwiększenia wartości zmiennej o 2, zmienna inkrementowana jest jedynie o 1. Przykład takiego zachowania zaobserwowaliśmy w aplikacji przedstawionej na listingach 3.8 i 3.9. Rozwiązaniem, jest zastosowanie pewnego rodzaju sekcji krytycznej, w której czas procesora nie może zostać przekazany, póki aktualny wątek nie wykona wszystkich powierzonych mu zadań. Tym sposobem jesteśmy pewni, że wątek podczas jednej, nieprzerwanej operacji, odczyta, zmieni i zapisze wartość zmiennej.

3.5 System.Threading 23 lock Z pomocą przychodzi oczywiście biblioteka klas.net Framework i przestrzeń nazw System.Threading. Służy do tego słowo kluczowe lock, a poprawiony przykład z jego użyciem wyglądałby następująco: public class Program // zmienna statyczna - mamy pewność // że istnieje tylko jedna instancja private static int _ counter ; // interesująca nas akcja public static class Increment () for ( int i =0; i <=10000; ++i) // sekcja określona słowem kluczowym lock // ograniczona jest tylko do jednego wątku lock ( this ) _ counter = _ counter + 1;... Listing 3.11: Odwołanie do zmiennej z uwzględnieniem wielu wątków Tak poprawiony kod przy każdym uruchomieniu zwróci oczekiwany przez nas wynik 100000. Przedstawiona konstrukcja synchronizuje dostęp wątków do obiektu, pozwalając na wejście do sekcji tylko jednemu z nich. Parametrem funkcji lock jest obiekt względem którego synchronizujemy. Wspomniany lock jest najprostszym sposobem synchronizacji wykonania wielu wątków w aplikacji. Jeżeli podejrzymy kod pośredni IL wygenerowany dla wywołania z listingu 3.11 zauważymy następującą instrukcję: call void [mscorlib]system.threading.monitor::enter(object, bool&) Wynika z tego, że lock jest jedynie nakładką (tzw. code sugar ) na wywołanie klasy System.Threading.Monitor. Klasa Monitor Klasa Monitor z przestrzeni nazw System.Threading, jak zostało już wspomniane oferuje możliwość synchronizacji dostępu do zmiennych przez kolejne wątki aplikacji. Udostępnia ona dużo więcej możliwośći niż omawiana już konstrukcja lock. Przyjrzyjmy się podstawowym metodom składającym się na klasę Monitor.

24 Rozdział 3..NET Framework Nazwa Enter TryEnter Exit Wait Pulse PulseAll Tabela 3.5: Podstawowe metody obiektów klasy Monitor Opis Uruchamia blokadę na danym obiekcie i rozpoczyna sekcję krytyczną Próbuje przejąć blokadę na danym obiekcie, informując jednocześnie o powodzeniu operacji Zwalnia blokadę na obiekcie i zamyka sekcję krytyczną Zwalnia blokadę na obiekcie i zatrzymuje wykonywanie wątku Powiadamia następny wątek w kolejce, o zmianie stanu obiektu blokowanego Powiadamia wszystkie czekające wątki, o zmianie stanu obiektu blokowanego Listing 3.12 przedstawia podstawowe wykorzystanie klasy Monitor na znanym już przykładzie inkrementacji licznika. public class Program // zmienna statyczna - mamy pewność // że istnieje tylko jedna instancja private static int _ counter ; // obiekt służący do synchronizacji private static object o = new object (); // interesująca nas akcja public static class Increment () for ( int i =0; i <=10000; ++i) Monitor. Enter (o); try // w tej części kodu mamy pewność // że będzie wykonywana przez co // najwyżej jeden wątek _ counter = _ counter + 1; finally // korzystamy z bloku finally, by mieć // pewność zamknięcia sekcji, i uniknięcia // zakleszczeń Monitor. Exit (o);

3.5 System.Threading 25... Listing 3.12: Odwołanie do zmiennej z użyciem klasy Monitor Powyższy przykład jest dokładnym odzwierciedleniem kodu, wygenerowanego przez kompilator przy użyciu konstrukcji lock. Klasa Monitor oferuje jednak wiele dodatkowych możliwości. Jedną z nich jest możliwość zareagowania, na sytuację w której inny wątek już blokuje dostęp do zasobów. Ilustruje to listing 3.13. public static void Sample () // przez 1000 ms( 1 sekunda ) próbujemy // przejąć blokadę i dostać się do sekcji // krytycznej if ( Monitor. TryEnter (o, 1000) ) try // część kodu wymagająca synchronizacji finally Monitor. Exit (o); else // akcja, gdy przejęcie blokady zakończyło się // niepowodzeniem // np komunikat z informacją Listing 3.13: Przykład użycia metody Monitor.TryEnter 3.5.3 Klasa ThreadPool Omawiając wielowątkowość na platformie.net Framework, nie można pominąć klasy ThreadPool. ThreadPool jest zbiorem reużywalnych wątków, mogących wykonywać dowolne zadania w aplikacji. Słowo reużywalny jest tu kluczowe, gdyż istotą tej klasy, jest wykorzystywanie wciąż tych samych wątków, eliminując narzut na kosztowne operacje stworzenia i konfiguracji wątku. Tabela 3.6 przedstawia zestawienia metod pozwalający na korzystanie z klasy ThreadPool Omawiany w tym rozdziale przykład inkrementacji licznika z wykorzystaniem ThredPool wyglądałby jak na listingu 3.14. public class Program // zmienna statyczna - mamy pewność

26 Rozdział 3..NET Framework Tabela 3.6: Podstawowe metody statyczne klasy ThreadPool Nazwa Opis GetAvailableThreads Zwraca aktualnie dostępną liczbę wątków GetMaxThreads Zwraca maksymalną liczbę wątków jakie mogą zostać utworzone w puli GetMinThreads Zwraca minimalną ilość wątków jakie muszą być utworzone w puli QueueUserWorkItem Zgłasza kod do wykonania w wątku z puli SetMaxThreads Ustawia maksymalną ilość wątków w puli SetMinThreads Ustawia minimalną ilość wątków w puli // że istnieje tylko jedna instancja private static int _ counter ; private object _ synchronization = new object (); // akcja wykonywana w wątku z puli ; // parametr context służy przekazywaniu parametrów // do metody ( parametr musi być zdefiniowany, lecz // jego wykorzystanie jest opcjonalne public static class Increment ( object context ) for ( int i =0; i <=10000; ++i) lock ( _ synchronization ) _ counter = _ counter + 1; public static void Main () for ( int i =0; i <10; i ++) ThreadPool. QueueUserWorkItem ( new WaitCallback ( Increment )); // nie ma prostego sposobu na określenia zakończenia // zakolejkowanych zadań ; // Czekamy pewien okres czasu, by wątki zakończyły swoje // działanie Thread. Sleep (5000) ; // tu spodziewamy się wyniku // 10* 10000 = 100000 Console. WriteLine ( _counter );

3.5 System.Threading 27 Listing 3.14: Przykład użycia klasy ThreadPool

28 Rozdział 3..NET Framework 3.6 Parallel Extensions Parallel Extensions 8 jest biblioteką składającą się na.net Framework 4.0, mającą ułatwić i usprawnić proces tworzenia aplikacji wielowątkowych. Składają się na nią trzy podstawowe elementy: Task Parallel Library(TPL) - biblioteka zawierająca klasy i metody służące do budowania aplikacji wielowątkowych PLINQ - uwzględniającą równoległość, implementację technologii LINQ Coordination Data Structure(CDS) - zespół klas ułatwiających współdzielenie danych Parallel Extensions powstało jako odpowiedź firmy Microsoft na panujące tendencje w dziedzinie inżynierii oprogramowania. Celem było dostarczenie programistom narzędzia, które niewielkim nakładem sił, a przede wszystkim bez ogromnych zmian w kodzie, pozwoli im wykorzystać całą moc dominujących dziś procesorów wielordzeniowych. Parallel Extensions tak naprawdę bazuje na omawianej już klasie Thread. Cała magia rozwiązania opiera się na obiekcie nazwanym planistą zadań(task Scheduler). Obiekt ten na podstawie szeregu algorytmów przydziela zlecone zadania(task), czyli fragmenty kodu mające wykonać się równolegle, do konkretnych wątków systemowych (reprezentowanych przez obiekty klasy Thread). Główny zysk, to brak przełączania kontekstu, ponieważ planista wykorzystuje wciąż te same wątki do wykonywania kolejnych zadań. Prócz czasu, zyskujemy również mniejsze zapotrzebowanie na pamięć operacyjną. Działanie planisty najlepiej zilustruje rysunek 3.3 Rysunek 3.3: Ilustracja pracy planisty zadań Jak widać na ilustracji 3.3 planista tworzy pewną ilość wątków (ilość ta zależna jest od ilości procesorów lub rdzeni), po czym przydziela im konkretne zadania, dbając jednocześnie, by podział 8 Znane również pod nazwą ParallelFX

3.6 Parallel Extensions 29 pracy był jak najbardziej równomierny. 3.6.1 Klasa Task Obiekty klasy Task(należącej do przestrzeni System.Threading.Tasks) reprezentują zbiór instrukcji majacych wykonać się w oddzielnym wątku. Pod względem zapewnianej funkcjonalności można go porównywać z klasą System.Threading.Thread (możliwość ta zostanie skrzętnie wykorzystana w dalszej części rozdziału). Listing 3.15 przedstawia różne sposoby tworzenia zadań. public class TaskExample public static void Main () // tworzymy zadanie z użyciem klasy Action Task task1 = new Task ( new Action ( DoWork )); // zadanie określone za pomocą delegatu Task task2 = new Task ( delegate DoWork (); ); // dwie konstrukcje korzystające // z wyrażeń Lambda Task task3 = new Task (() => DoWork ()); Task task4 = new Task (() => DoWork (); ); // wykorzystanie fabryki zadań // tak stworzony wątek zostaje od razu // zakolejkowany do uruchomienia, // nie mamy do niego referencji Task. Factory. StartNew (() => DoWork ()); // uruchamiamy zadania task1. Start (); task2. Start (); task3. Start (); task4. Start (); // czekamy na reakcję użytkownika Console. ReadLine ();

30 Rozdział 3..NET Framework // prosta metoda służąca za przykład // instrukcji wykonywanych w wątku public void DoWork () // nie interesuje nas nic więcej // poza wypisaniem tekstu na konsoli Console. WriteLine (" Hello!"); Listing 3.15: Przykłady tworzenia nowych zadań(task) Listing 3.15 przedstawia różne wariacje tworzenia i określania zadań, ale można z nich wyróżnić dwa podstawowe podejścia: - z użyciem obiektów Task, - z wykorzystaniem fabryki zadań. Oba rozwiązania różni przede wszystkim dostępność referencji do utworzonego zadania. Co jednocześnie predestynuje użycie konstrukcji Task.Factory.StartNew dla zadań o krótkim czasie życia. Prezentowane zadania(task) możemy także tworzyć z określonym stanem(parametrem). Przedstawia to listing 3.16. public class TaskExample public static void Main () // tworzymy zadanie z użyciem klasy Action Task task1 = new Task ( new Action < object >( DoWork ), " Jaś "); // zadanie określone za pomocą delegatu Task task2 = new Task ( delegate ( object param ) DoWork ( param );, " Staś "); // dwie konstrukcje korzystające // z wyrażeń Lambda Task task3 = new Task (( param ) => DoWork ( param ), " Krzyś "); Task task4 = new Task (( param ) => DoWork ( param );, " Gucio "); // wykorzystanie fabryki zadań // tak stworzony wątek zostaje od razu // zakolejkowany do uruchomienia, // nie mamy do niego referencji Task. Factory. StartNew (() => DoWork ());

3.6 Parallel Extensions 31 // uruchamiamy zadania task1. Start (); task2. Start (); task3. Start (); task4. Start (); // czekamy na reakcję użytkownika Console. ReadLine (); // prosta metoda służąca za przykład // instrukcji wykonywanych w wątku public void DoWork ( string name ) // nie interesuje nas nic więcej // poza wypisaniem tekstu na konsoli Console. WriteLine ( String. Format (" Hej! Mam na imię 0 ",name. ToString ())); Listing 3.16: Tworzenia zadań(task) z parametrem Po uruchomieniu aplikacji na konsoli powinien pojawić się następujący tekst: Hej! Mam na imię Jaś Hej! Mam na imię Staś Hej! Mam na imię Krzyś Hej! Mam na imię Gucio W równie prosty sposób mamy możliwość zebrania wyników zadania. Wystarczy, że w ciele metody zadania użyjemy słowa kluczowego return, a następnie skorzystamy z właściwości o nazwie Result z odpowiedniego obiektu Task. Całość przedstawia listing??. public class TaskExample public static void Main () // tworzymy zadanie, określając, że zwracanym // typem wartości jest int Task <int > task1 = new Task <int >( new Action ( DoWork )); // uruchamiamy zadanie task1. Start (); // wypisujemy wynik na konsoli // wykonynanie tej instrukcji, zostanie wstrzymane // dopóki wynik zadania nie będzie dostępny Console. WriteLine ( String. Format (" Wynik : 0 ", task1. Result )); Console. ReadLine ();

32 Rozdział 3..NET Framework // prosta metoda służąca za przykład // instrukcji wykonywanych w wątku public int DoWork () int a = 2; int b = 2; int wynik = a + b; return wynik ; Listing 3.17: Przykład pobrania wyniku działania zadania Przerywanie zadań Klasa Task daje nam możliwość przerwania już rozpoczętego zadania. Funkcjonalność ta jest bardziej rozbudowana, niż w klasie Thread, lecz jednocześnie lepiej zorganizowana i bardziej logiczna. Wszystko opiera się o obiekty klasy CancelationToken, które stanowią swego rodzaju żeton krążący między wątkiem głównym, a zadaniem. Listing 3.18 public class TaskExample public static void Main () // tworzymy " fabrykę " żetonów CancellationTokenSource tokensource = new CancellationTokenSource (); // pobieramy obiekt żetonu CancellationToken token = tokensource. Token ; // tworzymy zadanie, jako parametr przekazując // utworzony wcześniej żeton Task task1 = new Task ( new Action < CancellationToken >( DoWork ), token ); // uruchamiamy zadanie task1. Start (); // czekamy 0,5 sekundy Thread. Sleep (500) ; // przerywamy wątek tokensource. Cancel (); Console. ReadLine ();

3.6 Parallel Extensions 33 // prosta metoda służąca za przykład // instrukcji wykonywanych w wątku public void DoWork ( CancellationToken token ) // pętla wypisująca tekst w konsoli, póki // działanie wątku nie zostanie przerwane while ( true ) // jeżeli zgłoszono żądanie przerwania zadania if( token. IsCancelationRequested ) // wykonujemy kod zapewniający integralność // aplikacji ( o ile jest wymagany ) // i wyrzucamy poniższy wyjątek, informując // CLR o przerwaniu zadania throw new OperationCanceledException ( token ); Console. WriteLine (" Wciąż pracuję... "); Listing 3.18: Anulowanie zadania Korzystanie z obiektów CancellationToken niesie ze sobą wiele dobrego. Przekazując ten sam obiekt do wielu zadań, możemy jedną istrukcją przerwać wszystkie wykonywane w tym momencie wątki. W prosty sposób możemy także uzależnić działanie zadania od wielu żetonów. Służy do tego statyczna metoda CreateLinkedTokenSource. // tworzymy poszczególne źródła żetonów CancellationTokenSource tsource1 = new CancellationTokenSource (); CancellationTokenSource tsource2 = new CancellationTokenSource (); CancellationTokenSource tsource3 = new CancellationTokenSource (); // tworzymy zbiorczy żeton CancellationTokenSource linkedtoken = CancellationTokenSource. CreateLinkedTokenSource ( tsource1. Token, tsource2. Token, tsource3. Token ); // tak przygotowany obiekt, reaguje na wywołanie przerwania wątku // z każdego ze źródeł tsource CancellationToken token = linkedtoken. Token ; Listing 3.19: Tworzenie złożonych obiektów CancellationToken 3.6.2 Współdzielenie danych Jak zostało już zaznaczone, całe Parallel Extensions i obiekty klasy Task z których korzystamy, opierają się na omawianych już obiektach klasy Thread z przestrzeni System.Threading. Z tego

34 Rozdział 3..NET Framework powodu, do synchronizacji wątków i dostępu do danych korzystamy z tych samych klas, które zostały omówione w rozdziale 3.5.2. 3.6.3 Dodatki Rozdział ten równie dobrze mógłby zostać nazwany Wielowątkowość w służbie programisty. Microsoft tworząc API Parallel Extensions starał się ułatwić programistom wykorzystanie mocy drzemiącej w procesorach wielordzeniowych do granic możliwości. W dalszej części rozdziału, postaram się przedstawić szereg konstrukcji, które mają za zadanie uprościć programistom tworzenie aplikacji wielowątkowych. Lazy<T> Podczas tworzenia aplikacji, zdarzają się fragmenty kodu, które są wymagające obliczeniowo, bądź wykorzystują dużą ilość pamięci operacyjnej, lecz rezultat ich wykonania nie jest potrzebny przy każdym uruchomieniu aplikacji lub nie musi być wyświetlany na żądanie w czasie rzeczywistym. W takiej sytuacji, często takie obliczenia zlecane są wątkom w tle i uruchamiane tylko wtedy, gdy rzeczywiście są potrzebne, co oszczędza zasoby systemowe. W.NET Framework 4.0 Microsoft udostępnił konstrukcję przewidzianą dokładnie dla tego typu scenariuszy - klasę Lazy. Podczas inicjalizacji obiektu tej klasy, definiujemy kod, jaki zostanie wykonany w tle podczas pierwszego dostępu do zmiennej. Zwalnia to programistę z pisania kodu odpowiedzialnego za stworzenie wątku i jego synchronizację. Przykład zastosowania klasy Lazy przedstawia listing 3.20. public class Sample public static void Main ( string [] args ) // tworzymy obiekt klasy Lazy, inicjując go zadaniem // wykonywanym w tle, a zwracającym obiekt typu string Lazy <Task < string >> lazyvariable = new Lazy <Task < string > >( () => Task < string >. Factory. StartNew ( // poniżej definiujemy kod prowadzący do obliczenia // potrzebnej wartości () => // szereg // długotrwałych // obliczeń return " wynik "; )); // w tym momencie wartość zmiennej lazyvariable nie jest określona, // a kod w niej zawarty nie został wykonany Console. WriteLine (" Wynikiem działania aplikacji jest :"); // dopiero wywołanie poniższej instrukcji wykona kod zdefiniowany // podczas tworzenia zmiennej lazyvariable, a niniejszy wątek

3.6 Parallel Extensions 35 // zostanie wstrzymany, aż do chwili uzyskania wyniku Console. WriteLine ( lazyvariable. Value. Result ); Listing 3.20: Przykład użycia klasy Lazy Pętle równoległe (Parallel Loops) Pętle równoległe są jednym z najciekawszych elementów wprowadzonych w Parallel Extensions. Zacznijmy od prostego przykładu. public static void Main ( string [] args ) int [] numbers = new int []1,2,3,4,5; for ( int i =0; i< numbers. Lenght ; i ++) PerformLongCalculations ( numbers [ i]); foreach ( var num in number ) PerformLongCalculations ( num ); Parallel. For (0, numbers. Lenght, (i)=> PerformLongCalculations ( numbers [ i]); ); Parallel. Foreach ( numbers, num => PerformLongCalculations ( num ); ); Listing 3.21: Przykład użycia pętli Parallel.For Listing 3.21 przedstawia cztery sposoby przetworzenia elementów tablicy. Pierwsze dwie pętle to standardowe sekwencyjne pętle, podstawa każdej aplikacji. Dwie kolejne, to ich odpowiedniki w świecie ParallelFX. Co zyskujemy przez ich użycie? Tak pożądaną przez Nas wielowątkowość. Małym nakładem sił (różnica w konstrukcji pętli sekwencyjnej i jej wielowątkowego odpowiednika jest doprawdy kosmetyczna), możemy przerobić nasz kod tak, by w pełni wykorzystał moc wielordzeniowych procesorów, a tym samy skrócić czas jego wykonania (ku uciesze użytkowników).

36 Rozdział 3..NET Framework Pętle równoległe opierają się na użyciu omawianej już klasy Task. W gruncie rzeczy, każdy programista mógłby napisać taką instrukcję sam, lecz Microsoft już nam ją zapewnił. Istota działania Parallel.For i Parallel.Foreach jest bardzo prosta. W pierwszym kroku, zadanie zostaje podzielone na partycje(chunk lub partition), których ilość zależna jest od ilości wątków, jakie może rónolegle przetworzyć nasz procesor. Następnie, zawartość każdej z partycji zostaje przydzielona do osobnego zadania(task) i wykonana. O resztę dba planista zadań (Task Scheduler), który jak zostało już omówione, rodziela zadania pomiędzy fizyczne wątki. Dzięki temu, możemy w bardzo prosty i szybki sposób przystosować naszą aplikację do współczesnych standardów, nie odwołując się jawnie do tworzenia i zarządzania wątkami. Pamiętać należy, iż przedstawione przykłady dotyczą przypadków, w których każdy element z kolekcji przetwarzany jest niezależnie. W przeciwnym wypadku sami musimy zapewnić synchronizację dostępu do współdzielonych danych. Choć konstrukcja pętli równoległych jest stosunkowo prosta i logiczna, sposób przerwania ich wykonywania wymaga komentarza. Listing 3.22 przedstawia przykład przerwania wykonania sekwencyjnej pętli foreach public static void Main ( string [] args ) int [] numbers = new int []1,2,3,4,5; foreach ( var num in number ) if(num == 3) // w tym momencie wykonanie pętli // zostanie przerwane, a przetworzone // zostaną jedynie elementy 1 i 2 break ; PerformLongCalculations ( num ); Listing 3.22: Przykład przerwania pętli Foreach Odpowiadający kod z użyciem pętli równoległej, wyglądałby mniej więcej jak przedstawiony na listingu 3.23 public static void Main ( string [] args ) int [] numbers = new int []1,2,3,4,5; Parallel. Foreach ( numbers, ( int num, ParallelLoopState state ) => if(num == 3)

3.6 Parallel Extensions 37 state. Break (); PerformLongCalculations ( num ); ); Listing 3.23: Przykład przerwania pętli Parallel.Foreach 3.6.4 Planista - Task Scheduler Wielokrotnie, zaznaczane było, iż działanie zadań(task) w Parallel Extensions opiera się na wykorzystaniu obiektów klasy Thread znanych od początków.net Framework. Co zatem wyróżnia nowe rozwiązanie Microsoftu? Bo przecież nie kilka udogodnień składniowych, przedstawionych w poprzednim rozdziale. Kluczem jest tu planista zadań(task Scheduler). Planista, stanowi warstwę pośrednią pomiędzy zadaniem(task), a obiektem klasy Thread, reprezentującym wątek systemowy. Jak sama nazwa wskazuje, planuje on wykonanie kolejnych zadań, tak by wykonały się one w jak najszybszym czasie, wykorzystując maksimum dostępnych zasobów. Zmiennymi w tym procesie są między innymi ilość rdzeni, bądź procesorów, co przekłada się na ilość wątków wykonania procesora, długość poszczególnych zadań, parametry określone przez programistę..net Framework 4 zawiera implemenetacje kilku różnych planistów, które możemy wybrać stosownie do potrzeb. Domyślny planista, do dysponowania zadań pomiędzy wątkami, korzysta z algorytmu wspinaczki na szczyt (hill-climbing algorithm) 9. Z omawianym planistą związane są dwie istotne właściwości tworzonych zadań (Task) - TaskCreationOptions i TaskStatus. TaskCreationOptions pozwala wpływać na interpretacje tworzonego zadania przez planistę (możliwe do zdefiniowania wartości przedstawia tabela 3.7). TaskStatus zawiera informacje o aktualnym stanie zadania (każdy status został przedstawiony w tabeli 3.8. Nazwa None PreferFairness LongRunning AttachedToParent Tabela 3.7: Wartości TaskCreationOptions Opis Domyślne ustawienia Sugeruje traktowanie zadania jak najbardziej sprawiedliwie Określa zadanie jako długofalowe Definiuje zadanie jako podległe w hierarchii 9 Algorytm wspinaczki na szczyt można zaliczyć do heurystycznych metod optymalizacji. Algorytm polega na przeszukiwaniu przestrzeni rozwiązań w poszukiwaniu rozwiązania optymalnego, poprzez porównywanie jakości kolejnych rozwiązań

38 Rozdział 3..NET Framework Tabela 3.8: Wartości TaskStatus Nazwa Opis Created Zadanie zostało stworzone, lecz jego wykonanie nie zostało zaplanowane WaitingForActivation Zadanie czeka na plan wykonania WaitingToRun Zadanie zostało zaplanowane, ale nie uruchomione Running Zadanie w toku WaitingForChildrenToComplete Zadanie czeka na wykonanie zadań potomnych RanToCompletion Zadanie wykonane bez zastrzeżeń Canceled Wykonywanie zadania zostało przerwane Faulted Wykonanie zadania zakończyło się wystąpieniem wyjątku

Część II Część praktyczna

Rozdział 4 Parallel Image Effects 4.1 Opis aplikacji Parallel Image Effects jest prostą aplikacją ilustującą wykorzystanie omawianiej biblioteki Paralell Extensions w praktyce. Program umożliwia zastosowanie różnego rodzaju filtrów i efektów na wczytanych uprzednio plikach graficznych. Rysunek 4.1 przedstawia ekran główny programu. Rysunek 4.1: Okno główne programu Ekran główny podzielony jest na dwie zasadnicze części: - pasek narzędziowy, - przestrzeń zakładek, - pasek statusu. Pasek narzędziowy zawiera przyciski udostępniające konkretne akcje podzielone na zakładki. Resztę okna stanowi podgląd aktualnie edytowanego obrazka. Aplikacja pozwala operować na wielu plikach graficznych jednocześnie - każdy plik stanowi oddzielną fiszkę. Dla ułatwienia nawigacji, każda zakładka zawiera nazwę wyświetlanego pliku, a pasek statusu pełną

42 Rozdział 4. Parallel Image Effects ścieżkę dostępu do pliku na dysku. 4.2 Funkcjonalności Pasek narzędziowy podzielony został na cztery zakładki. Pierwsza - Główne - zawiera grupy funkcji Plik i Podstawowe Operacje. Kolejne trzy zakładki zawierają te same funkcjonalności, jednak realizowane odpowiednio: - przez jeden wątek wykonawczy, - przez wiele wątków z użyciem przestrzeni nazw System.Threading, - przez wiele wątków z użyciem biblioteki Parallel Extensions. Grupa Plik Otwiera nową pustą zakładkę. Nowa zakładka Otwórz Otwiera systemowe okno dialogowe wyboru plików, które pozwala wybrać plik graficzny do edycji w aktualnie zaznaczonej zakładce. Jeżeli żadna zakładka nie została jeszcze otwarta, aplikacja sama ją utworzy i otworzy wskazany plik graficzny. Otwórz w nowej zakładce Otwiera systemowe okno dialogowe wyboru plików, które pozwala wybrać plik graficzny do edycji, a następnie umieszcza go w nowo utworzonej zakładce. Zapisz Zapisuje zmiany jakie zaszły w pliku graficznym.

4.2 Funkcjonalności 43 Zapisz jako... Pozwala zapisać obrazek wraz z naniesionymi zmianami w innym pliku na dysku (plik docelowy określamy z pomocą systemowego okna dialogowego). Zapisz wszystkie Zapisuje zmiany we wszystkich otwartych zakładkach. Grupa Podstawowe Operacje Zmień rozmiar Otwiera okno dialogowe pozwalające zmienić rozmiar edytowanego pliku graficznego. Odbicie poziome Dokonuje transformacji obrazka do jego odbicia w poziomie. Odbicie pionowe Dokonuje transformacji obrazka do jego odbicia w pionie. Odbicie skośne Dokonuje transformacji obrazka do jego odbicia w pionie i w poziomie jednocześnie. Grupa Barwy Negatyw Tworzy negatyw z wyświetlanego pliku graficznego.

44 Rozdział 4. Parallel Image Effects Negatyw danego obrazka tworzymy zastępując wartości składowych RGB pikseli, na różnicę 255 w, gdzie w stanowi wyjściąwoą wartość danej składowej koloru. Kontrast/Jasność Otwiera okno dialogowe (4.2) pozwalające zmienić kontrast i jasność edytowanego pliku graficznego. Rysunek 4.2: Okno dialogowe Jasność/Kontrast Operacja zmiany jasności sprowadza się do dodania, bądź odjęcia pewnej stałej od każdej składowej koloru (RGB) w każdym pikselu składającym się na przetwarzany plik graficzny. Proces zmiany kontrastu w obrazie prowadzi do zmiany relatywnych wartości pomiędzy sąsiednimi pikselami. W aplikacji Parallel Image Effects do osiągnięcia tego celu w optymalny sposób wykorzystano klasę ColorMatrix. Reprezentuje ona macierz przekształcenia, która zostaje zastosowana na wszystkich pikselach przetwarzanego obrazu. Zastosowana macierz prezentuje się następująco: C 0 0 0 0 0 C 0 0 0 0 0 C 0 0 0 0 0 1 0 0 0 0 0 1 C w tym przypadku stanowi wartość o jaką chcemy przeskalować wartości pikseli. Aby zachować obrazek w oryginalnym stanie stosujemy C = 1, C < 1 zmniejsza kontrast, C > 1 analogicznie zwiększa kontrast.

4.2 Funkcjonalności 45 Filtr alfa-obcięty Otwiera okno dialogowe (rys. 4.3) pozwalające zastosować filtr alfa-obcięty na edytowanym pliku graficznym. Rysunek 4.3: Okno dialogowe obsługi filtru alfa-obciętego Filtr alfa-obcięty służy do reduckcji szumu w pliku graficznym. Jego działanie prezentuje rysunek 4.3. Plik graficzny przetwarzany jest zgodnie ze wzorem 4.1. F (x, y) = 1 mn d (s,t)es xy f(s, t) (4.1) F (x, y) określa wartość piksela w punkcie x, y po przetworzeniu, S xy określa sąsiedztwo przetwarzanego piksela o szerokości m i wysokości n. f(s, t) to wartość piksela w oryginalnym obrazie. Algorytm przetwarza plik graficznym piksel po pikselu, analizując oreślone parametrem sąsiedztwo. Nową wartość piksela stanowi średnia z wartości sąsiednich pikseli, po usunięciu d/2 najmniejszych i d/2 największych wartości. Filtr średniej kontrharmonicznej Otwiera okno dialogowe (rys. 4.4) pozwalające zastosować filtr średniej kontrharmonicznej na edytowanym pliku graficznym. Filtr średniej kontrharmonicznej służy do odszumiania plików graficznych. Jego działanie opiera się na zmianie wartości pikseli bitmapy, na równe średniej kontrharmonicznej określonego sąsiedztwa

46 Rozdział 4. Parallel Image Effects Rysunek 4.4: Okno dialogowe obsługi filtru średniej kontrharmonicznej piksela, zgodnie ze wzorem 4.2. F (x, y) = (s,t) S xy f(s, t) Q+1 (s,t) S xy f(s, t) Q (4.2) Q w tym przypadku jest parametrem filtru i nosi miano rzędu filtru. Filtr medianowy Otwiera okno dialogowe (rys. 4.5) pozwalające zastosować filtr medianowy na edytowanym pliku graficznym. Filtr medianowy jest kolejnym sposobem na wyeliminowanie szumu z pliku graficznego. Jego działanie jest stosunkowo proste - wartość danego piksela zamieniana jest na medianę wartości sąsiednich pikseli, zgodnie ze wzorem 4.3. F (x, y) = median (s,t) Sxy f(s, t) (4.3) Wartość mediany wyliczamy ustawiając wartości pikseli z sąsiedztwa w uporządkowanym ciągu, a następnie wybieramy element środkowy. Operator Rozenfelda

4.2 Funkcjonalności 47 Rysunek 4.5: Okno dialogowe wywołania filtru medianowego Otwiera okno dialogowe (rys. 4.6) pozwalające określić parametry operatora Rozenfelda, który następnie zostanie nałożony na edytowany plik graficzny. Rysunek 4.6: Okno dialogowe operatora Rozenfelda Operator Rozenfelda może służyć jako narzędzie do wykrywania krawędzi na bitmapach. Wartość każdego piksela obrazka, zamieniana jest na wartość średnią z sąsiadujących pikseli zgodnie ze wzorem 4.4. F (x, y) = (1/P )f(x + P 1, y) + f(x + P 2, y) +... + f(x, y) f(x 1, y) f(x 2, y)... f(x P, y) (4.4)

48 Rozdział 4. Parallel Image Effects Szczegóły tła Otwiera okno dialogowe (rys. 4.7) pozwalające zdefniować parametry operacji splotu, dzięki której możemy wydobyć szczegóły tła edytowanego obrazka. Rysunek 4.7: Okno dialogowe operacji wydobywania szczegółów tła Splot definiujemy za pomocą równania 4.5. M M F (x, y) = h(i, j)f(x + i, y + j), x = M, 2,..., P M 1, y = M, 2,..., P M 1 (4.5) i= M j= M W aplikacji zaimplementowane zostały następujące maski splotu h(i, j): Maska południe Maska zachód 4.3 Budowa projektu 4.3.1 Technologie i narzędzia h(, ) = h(, ) = 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1 1 1 1.. Aplikacja Parallel Image Effects została napisana z wykorzystaniem.net Framework 4.0 i technologii Windows Presentation Foundation(WPF). Jako IDE posłużyło Microsoft Visual C# 2010 Express. Dodatkowo do stworzenia paska narzędzi zgodnego z Fluent User Interface (znanego m.in. z pakietu

4.3 Budowa projektu 49 Microsoft Office 2007) została wykorzystana biblioteka WPF Ribbon Preview, którą można odnaleźć pod adresem http://wpf.codeplex.com/wikipage?title=wpf%20ribbon%20preview&projectname= wpf. 4.3.2 Budowa projektu Kod aplikacji został podzielony na następujące projekty: Common Projekt dostępny w każdym elemencie aplikacji. Zawiera zestaw metod pomocniczych i Extension Methods. Składowe: - Extensions.cs Zbiór Extension Methods ułatwiających operację na plikach graficznych. - Helpers.cs Zbiór metod pomocniczych. - NativeMethods.cs Odwołania do WinAPI. Filters Zawiera szereg interfejsów wywołań filtrów i efektów graficznych, które następnie są implementowane w różnych konfiguracjach obsługi wątków. Zawiera także implementację podstawowych operacji graficznych, jak odbicia, czy zmiana rozmiaru. Filters.ParallelFX Implmentacja filtrów i efektów graficznych w oparciu o Parallel Extensions. Składowe: - AlphaTrimmedFilter.cs Implementacja filtru alfa-obciętego. - ContraharmonicMeanFilter.cs Implementacja filtru średniej kontrharmonicznej. - ConvolutionFilter.cs Implmenetacja operacji splotu i wydobywania szczegółów tła. - MedianFilter.cs Implmenetacja filtru medianowego. - RozenfeldOperator.cs Implementacja operatora Rozenfelda. - SimpleTransforms.cs Implementacja prostych przekształceń (np. zmiana jasności). Filters.Single Projekt zawiera implementację filtrów i efektów bez użycia wielowątkowości, w pełni sekwen-

50 Rozdział 4. Parallel Image Effects cyjnie. Na projekt składają się te same elementy, które pokrótce omówiono w powyższym punkcie. Filters.Threads Implementacja filtrów i operatorów z zastosowaniem wielowątkowości w oparciu o elementy przestrzeni nazw System.Threading (m.in. klasa Thread, czy ThreadPool). Elementy składowe zostały omówione w punkcie opisującym Filters.ParallelFX ParallelImage Ten projekt skupia implementację interfejsu aplikacji pozwalającej operować na plikach graficznych z wykorzystaniem wyżej opisanych projektów. Składowe: - App.xaml i App.cs Standardowo generowana klasa reprezentująca całą aplikację. Stanowi jednocześnie punkt wejściowy programu. - MainWindow.xaml i MainWindow.cs Pliki określają wygląd i logikę działania głównego okna aplikacji. - Dialogs Zbiór definicji okien dialogowych pojawiających się w aplikacji. - Dialogs\ConvolutionDialog.xaml i ConvolutionDialog.cs Opis i implementacja logiki okna dialogowego obsługi operacji splotu. - Dialogs\FilterDialog.xaml i FilterDialog.cs Opis i implementacja okna dialogowego obsługującego filtry redukujące szum. - Dialogs\RozenfeldDialog.xaml i RozenfeldDialog.cs Implementacja okna dialogowego określającego parametry wywołania operatora Rozenfelda. - Dialogs\SimpleTransformDialog.xaml i SimpleTransformDialog.cs Opis i implementacja okna podstawowych przekształceń. - Helpers\TabInfo.cs Klasa pomocnicza agregująca informacje o otwarych zakładkach. - Images Zawiera pliki graficzne wykorzystywane w interfejsie. - Localization\UIStrings.resx Słownik par klucz-wartość wykorzystywanych do lokalizacji aplikacji. SynteticTests Ten projekt zawiera klasy i metody służące do testowania rozwiązań w oparciu o przestrzeń nazw System.Threading i Parallel Extensions. Zawiera m.in. implmenetację testu przechodzenia drzewa binarnego, czy rysowania zbioru Mandelbrota opisanych w rozdziale 5. 4.3.3 Diagram UML Rysunek 4.8 przedstawia diagram klas implementujących filtry i operatory graficzne.

4.3 Budowa projektu 51 Rysunek 4.8: Diagram klas implementujących filtry i operatory graficzne 4.3.4 Szczegóły implementacji Poniżej przedstawiam istotniejsze elementy implementacji. Oczekiwanie na zakończenie wątków z puli W wielu miejscach aplikacji, w celu jak najlepszej optymalizacji kodu wykorzystującego przestrzeń nazw System.Threading wykorzystałem wątki z puli ThreadPool. Wadą tego rozwiązania jest brak referencji do wykorzystywanego wątku, przez co nie możemy w prosty sposób oczekiwać zakończenia jego pracy.

52 Rozdział 4. Parallel Image Effects Aby rozwiązać ten problem przygotowałem prostą metodę, która w pętli sprawdza, czy wątki z puli wciąż pracują. Przedstawia ją listing 4.1. /// <summary > /// Prosta metoda czekająca na zakończenie wątków w puli /// </ summary > public static void WaitForThreads ( int timeoutiterations ) int maxthreads = 0; int placeholder = 0; int availthreads = 0; while ( timeoutiterations > 0 timeoutiterations < 0) System. Threading. ThreadPool. GetMaxThreads ( out maxthreads, out placeholder ); System. Threading. ThreadPool. GetAvailableThreads ( out availthreads, out placeholder ); if ( availthreads == maxthreads ) break ; System. Threading. Thread. Sleep ( TimeSpan. FromMilliseconds (100) ); -- timeoutiterations ; DoEvents (); Listing 4.1: Implementacja metody WaitForThreads Przetwarzanie plików graficznych Przetwarzanie plików graficznych jest podstawą aplikacji Parallel Image Effects. Wielce istotnym elementem jest sposób dostępu do konkretnych pikseli i składowych kolorów. Podstawowym sposobem operowania na pliku graficznym jest klasa Bitmap i jej metody GetPixel(x,y) i SetPixel(x,y, color). Jednak jest to metoda niezwykle nieefektywna i nie nadaje się do przetwarzania grafiki. Aby uzyskać zadowalające efekty (głównie chodzi o czas wykonania) należy ulokować bitmapę w pamięci operacyjnej i operować na niej za pomocą wskaźników. Listing 4.2 przedstawia przykładową metodę, która operuje na bitmapie ulokowanej w pamięci. public Bitmap Operate ( Bitmap image ) // tworzymy kopię wejściowej bitmapy Bitmap outputbitmap = Helpers. CloneImage ( image ); // LockBits powoduje ulokowanie bitmapy w pamięci operacyjej

4.3 Budowa projektu 53 // i zwrócenie informacji potrzebnych do operowania na niej // w postaci obiektu klasy BitmapData BitmapData outputdata = outputbitmap. LockBits ( new Rectangle (0, 0, image. Width, image. Height ), ImageLockMode. ReadWrite, transformedimagepixelformat ); BitmapData inputdata = image. LockBits ( new Rectangle (0, 0, image. Width, image. Height ), ImageLockMode. ReadOnly, transformedimagepixelformat ); // zaznaczamy, że kod wykonywany dalej będzie odwoływał się // bezpośrednio do pamięci operacyjnej omijając mechanizm // GarbageCollector unsafe // właściwość Scan0 wskazuje na adres początku bitmapy // w pamięci operacyjnej byte * outputpointer = ( byte *) outputdata. Scan0 ; byte * inputpointer = ( byte *) inputdata. Scan0 ; // obliczamy jak dużą przestrzeń pamięci zajmuje jeden // wiersz bitmapy ( założyliśmy tu, że obrazek jest // zapisany w 24 bitach, czyli 3 bajtach int addedoffset = inputdata. Stride - image. Width * 3; int sizey = image. Height ; int sizex = image. Width ; // iterujemy po każdym wierszy for ( int y =0; y< sizey ; y ++) // i każdym pikselu w wierszy for ( int x = 0; x < sizex ; x ++) // do wskaźnika możemy odwołać się jak do tablicy. // Indeks 0 to kanał składowa czerwona ( R) // Indeks 1 to kanał składowa zielona ( G) // Indeks 2 to kanał składowa niebieska ( B) // poniżej dokonujemy negacji obrazka, czyli od 255 // odejmujemy każdą składową koloru, i tak otrzymane // wartości zapisujemy w bitmapie wyjściowej outputpointer [0] = ( byte ) (255 - ( int ) inputpointer [0]) ; outputpointer [1] = ( byte ) (255 - ( int ) inputpointer [1]) ; outputpointer [2] = ( byte ) (255 - ( int ) inputpointer [2]) ; // przeskakujemy o 3 bajty, czyli do następnego // piksela inputpointer += 3; outputpointer += 3;

54 Rozdział 4. Parallel Image Effects ); // przeskakujemy do kolejnego wiersza outputpointer += addedoffset ; inputpointer += addedoffset ; // po przetworzenie pozostaje jedynie odczytać wynik z // pamięci operacyjnej i " uwolnić " bitmapę image. UnlockBits ( inputdata ); outputbitmap. UnlockBits ( outputdata ); return outputbitmap ; Listing 4.2: Przykład operacji na bitmapie ulokowanej w pamięci operacyjnej

Rozdział 5 Testy 5.1 Operacje na drzewie binarnym Drzewo binarne jest strukturą danych opisanych na pewnym skończonym zbiorze węzłów, która: nie zawiera węzłów (mamy wtedy do czynienia z drzewem pustym) składa się z rozłączynych zbiorów węzłów: - korzeni - lewego poddrzewa - prawego poddrzewa. Każdy węzeł zawiera wskazanie do węzła nadrzędnego oraz wzkazania na prawe i lewe poddrzewo.[17] W aplikacjach wykorzystujących drzewa binarne często pojawia się potrzeba dokonania pewnych operacji na każdym węźle drzewa. To zadanie stanowi podstawę niniejszego testu. Listing 5.1 przedstawia przykładową implementację drzewa binarnego. /// <summary > /// Węzeł drzewa binarnego /// </ summary > public class TNode /// <summary > /// Lewe poddrzewo /// </ summary > public TNode LeftNode get ; set ; /// <summary > /// Prawe poddrzewo /// </ summary > public TNode RightNode get ; set ; /// <summary > /// Wartość związana z węzłem /// </ summary > public int Value get ; set ; /// <summary >

56 Rozdział 5. Testy /// Statyczna metoda tworząca instację drzewa binarnego. /// Tworzy drzewo o żądanej wysokości, przypisując każdemu /// węzłowi kolejną wartość całkowitą począwszy od określonej /// w parametrze. /// </ summary > /// <param name =" deep "> Wysokość </ param > /// < param name =" start " > Wartość początkowa licznika </ param > /// < returns > Korzeń stworzonego drzewa </ returns > public static TNode CreateTree ( int deep, int start ) TNode root = new TNode (); root. Value = start ; if ( deep > 0) root. LeftNode = CreateTree ( deep - 1, start + 1); root. RightNode = CreateTree ( deep - 1, start + 1); return root ; Listing 5.1: Implementacja drzewa binarnego Dalej zdefiniujmy klasę TreeTravel jak na listingu 5.2. /// <summary > /// Drzewo do przetworzenia /// </ summary > public TNode Tree get ; set ; public TreeTravel () Tree = TNode. CreateTree (9, 1); InitializeComponent (); /// <summary > /// Metoda imitująca długotrwałe obliczenia /// na elemencie drzewa /// </ summary > /// < param name =" value " > Wartość do przetworzenia </ param > /// <returns > Wynik </ returns > public static int ProcessItem ( int value ) Thread. SpinWait (4000000) ; return value ; /// <summary >

5.1 Operacje na drzewie binarnym 57 /// Przechodzenie drzewa /// </ summary > /// <param name =" node "> Węzeł początkowy </ param > public static void WalkTree ( TNode node ) Listing 5.2: Klasa TreeTravel Klasa TreeTravel składa się z trzech istotnych elementów: konstruktora, w którym tworzymy drzewo binarne o wysokości 4, metody ProcessItem, która imituje przetwarzanie wartości drzewa, metody WalkTree, której zadaniem jest odwiedzenie każdego węzła w drzewie i wywołanie ProcessItem na jego wartości. Najprostszym sposobem implementacji metody WalkTree jest wykorzystanie rekurencji, co przedstawia listing 5.3. /// <summary > /// Rekurencyjne przechodzenie drzewa /// </ summary > /// <param name =" node "> Węzeł początkowy </ param > public static void WalkTreeRecurrent ( TNode node ) if ( node == null ) return ; WalkTreeRecurrent ( node. LeftNode ); WalkTreeRecurrent ( node. RightNode ); ProcessItem ( node. Value ); Listing 5.3: Implementacja WalkTree za pomocą rekurencji Czas wykonania takiego zadania wynosi 3.0483576s.Wadę tego rozwiązania widać od razu po uruchomieniu menadżera zadań - wykorzystuje ono jedynie jedną jednostkę obliczeniową. Aby temu zaradzić, należy wprowadzić obsługę wielowątkowści. Drugie rozwiązanie problemu oparte zostało o obiekty klasy Thread. Prezentuje je listing 5.4. /// <summary > /// Przechodzenie drzewa z wykorzystaniem /// wątków /// </ summary > /// <param name =" node "> Węzeł początkowy </ param > public static void WalkTreeThread ( TNode node )

58 Rozdział 5. Testy if ( node == null ) return ; Thread left = new Thread (( o) => WalkTreeThread ( node. LeftNode )); left. Start (); Thread right = new Thread (( o) => WalkTreeThread ( node. RightNode )); right. Start (); left. Join (); right. Join (); ProcessItem ( node. Value ); Listing 5.4: Implmenetacja WalkTree z wykorzystaniem klasy Thread Czas wykonania tej wersji to 3.0692694s. Pozostaje wykorzystać Parallel Extension. Prezentuje to listing 5.5. /// <summary > /// Przechodzenie drzewa z wykorzystaniem /// Tasków /// </ summary > /// <param name =" node "> Węzeł początkowy </ param > public static void WalkTreeTasks ( TNode node ) if ( node == null ) return ; Task left = Task. Factory. StartNew (() => WalkTreeTasks ( node. LeftNode )); Task right = Task. Factory. StartNew (() => WalkTreeTasks ( node. RightNode ) ); left. Wait (); right. Wait (); ProcessItem ( node. Value ); Listing 5.5: Implementacja WalkTree z użyciem obiektów Task Czas wykonania takiego kodu to 3.0544437. Tabela 5.1 przedstawia zestawienie czasu przejścia drzewa o wysokości cztery w różnych konfiguracjach sprzętowych.

5.2 Zbiory Mandelbrota 59 Tabela 5.1: Czas odwiedzenia każdej gałęzi drzewa Wykonanie rekurencyjne Klasa Thread ParallelFX Procesor jednordzeniowy 3.0483576 3.0692694 3.0544437 Procesor dwurdzeniowy 3.8746301 11.5201821 03.7416921 Jednostka dwuprocesorowa 5.2 Zbiory Mandelbrota Kolejnym testem jest rysowanie zbioru Mandelbrota[18]. Jest to podzbiór płaszczyzny zespolonej, którego brzeg stanowi jeden z najbardziej znanych fraktali. Nazwę zawdzięcza swemu odkrywcy Benoit Mandelbrot. Zbiór możemy przedstawić jako ciąg opisany wzorem: z 0 = 0 z n+1 = zn 2 + p Sam fraktal stanowi brzeg omawianego zbioru. Odnajdujemy go wyliczając kolejne przybliżenia zbioru, które stanowi zbiór liczb zespolonych odpowiednio: - dla przybliżenia 1: wszystkie punkty - dla przybliżenia 2: z 1 < 2 - dla przybliżenia 3: z 1 < 2i z 2 < 2 -... - dla przybliżenia n: z 1 < 2i z 2 < 2,..., z n 1 < 2 (5.1) Przygotowana aplikacja testowa wyświetla fraktal Mandelbrota dla zadanych wartości parametru p, korzystając ze skali szarości do odróżnienia punktów nie należących do zbioru. Przykładowy, wygenerowany przez aplikację fraktal przedstawia rysunek 5.1. Przygotowane zostały trzy wersje kodu generującego fraktal: - sekwencyjny (listing 5.6) - z wykorzystaniem klasy Thread (listing 5.7) - z wykorzystaniem ParallelFX (listing 5.9) private Image SequentialGetBitmap ( double startx, double starty, double endx, double endy ) Color [] cs = new Color [256]; cs = GetColors (); Bitmap bitmap = new Bitmap ( this. picturebox. Width, this. picturebox. Height );

60 Rozdział 5. Testy Rysunek 5.1: Przykładowy fraktal Mandelbrota double x, y, x1, y1, xtemp, xmin, xmax, ymin, ymax = 0.0; int iteration = 0; double delatx, deltay = 0.0; xmin = startx ; ymin = starty ; xmax = endx ; ymax = endy ; delatx = ( xmax - xmin ) / this. picturebox. Width ; deltay = ( ymax - ymin ) / this. picturebox. Height ; x = xmin ; for ( int i = 1; i < this. picturebox. Width ; i ++) y = ymin ; for ( int j = 1; j < this. picturebox. Height ; j ++) x1 = 0; y1 = 0; iteration = 0; while ( iteration < 100 && Math. Sqrt (( x1 * x1) + ( y1 * y1)) < 2) iteration ++; xtemp = (x1 * x1) - (y1 * y1) + x; y1 = 2 * x1 * y1 + y; x1 = xtemp ;

5.2 Zbiory Mandelbrota 61 double percent = iteration / (100.0) ; int val = (( int )( percent * 255) ); bitmap. SetPixel (i, j, cs[val ]); y += deltay ; x += delatx ; return ( Image ) bitmap ; Listing 5.6: Sekwencyjne generowanie żuka Mandelbrota Bitmap bitmap = null ; Color [] cs = new Color [256]; private Image ThreadGetBitmap ( double startx, double starty, double endx, double endy ) cs = GetColors (); object padlock = new object (); bitmap = new Bitmap ( this. picturebox. Width, this. picturebox. Height ); ParamDTO parameters = new ParamDTO (); parameters. XMin = startx ; parameters. YMin = starty ; parameters. XMax = endx ; parameters. YMax = endy ; parameters. DeltaX = ( endx - startx ) / this. picturebox. Width ; parameters. DeltaY = ( endy - starty ) / this. picturebox. Height ; Thread [] threads = new Thread [ picturebox. Width ]; ParameterizedThreadStart threadstart = new ParameterizedThreadStart ( ThreadAction ); for ( int i = 1; i < this. picturebox. Width ; i ++) threads [ i] = new Thread ( threadstart ); ParamDTO nparams = ( ParamDTO ) parameters. Clone (); nparams. IterationCount = i; threads [i]. Start ( nparams );

62 Rozdział 5. Testy threads [i]. Join (); return ( Image ) bitmap ; /// <summary > /// Akcja wykonywana w wątku /// </ summary > /// < param name =" param " > Parametry aktualnej iteracji </ param > public void ThreadAction ( object param ) ParamDTO parameters = ( ParamDTO ) param ; double x = parameters. XMin + ( parameters. IterationCount - 1) * parameters. DeltaX ; double y = parameters. YMin ; for ( int j = 1; j < this. picturebox. Height ; j ++) double x1 = 0; double y1 = 0; int iteration = 0; while ( iteration < 100 && Math. Sqrt (( x1 * x1) + ( y1 * y1)) < 2) iteration ++; double xtemp = ( x1 * x1) - ( y1 * y1) + x; y1 = 2 * x1 * y1 + y; x1 = xtemp ; double percent = iteration / (100.0) ; int val = (( int )( percent * 255) ); lock ( parameters. Padlock ) bitmap. SetPixel ( parameters. IterationCount, j, cs[val ]); y += parameters. DeltaY ; Listing 5.7: Generowanie żuka Mandelbrota z wykorzystaniem klasy Thread private Image ParallelFXGetBitmap ( double startx, double starty, double endx, double endy ) Color [] cs = new Color [256];

5.2 Zbiory Mandelbrota 63 cs = GetColors (); object padlock = new object (); Bitmap bitmap = new Bitmap ( this. picturebox. Width, this. picturebox. Height ); double xmin, xmax, ymin, ymax = 0.0; double deltax, deltay = 0.0; xmin = startx ; ymin = starty ; xmax = endx ; ymax = endy ; deltax = ( xmax - xmin ) / this. picturebox. Width ; deltay = ( ymax - ymin ) / this. picturebox. Height ; Parallel. For (1, this. picturebox. Width, i => double x = xmin + ( i - 1) * deltax ; double y = ymin ; for ( int j = 1; j < this. picturebox. Height ; j ++) double x1 = 0; double y1 = 0; int iteration = 0; while ( iteration < 100 && Math. Sqrt (( x1 * x1) + ( y1 * y1)) < 2) iteration ++; double xtemp = ( x1 * x1) - ( y1 * y1) + x; y1 = 2 * x1 * y1 + y; x1 = xtemp ; double percent = iteration / (100.0) ; int val = (( int )( percent * 255) ); lock ( padlock ) bitmap. SetPixel (i, j, cs[val ]); ); y += deltay ; return ( Image ) bitmap ;

64 Rozdział 5. Testy Listing 5.8: Generowanie żuka Mandelbrota z wykorzystaniem ParallelFX Logika przedstawionego kodu, opierając się na wersji sekwencyjnej (listing 5.6) nie jest wyjątkowo zawiła. Dla każdego piksela generowanego obrazka wyliczamy zgodnie z równaniem 5.1 poziom przybliżenia i na jego podstawie wybieramy jeden z poziomów szarości, jakim kolorujemy rozpatrywany piksel. Problem pojawia się przy próbie zrównoleglenia operacji (listing 5.7). Po pierwsze pojawiają nam się dwa dodatkowe elementy: - metoda zawierająca kod do wykonania w oddzielnym wątku, - klasa opisująca parametry przekazywane do wątku. Oczywiście całość można zapisać w postaci bardziej skondensowanej, ale stracimy wtedy wiele z przejrzystości kodu. Ciało metody również zostało zmienione, zapewniając odpowiedni dostęp do współdzielonych danych. Dalej przechodzimy do listingu 5.9, gdzie to samo zadanie zostało wykonane z pomocą Parallel Extensions. Oprócz zapewnienia poprawnego dostępu do danych współdzielonych, zmianie uległa tylko jedna linia kodu. Delikatnie zmienił się sposób wywołania nadrzędnej pętli for. Ta instrukcja, pozwala nam wywołać każdą iterację pętli w osobnym zadaniu, które następnie zostaną rozdysponowane pomiędzy wątkami systemowymi. Poprzez zmianę jednej linijki kodu, wprowadziliśmy do aplikacji obsługę wielowątkowści. Sprawdźmy zatem jaki zysk daje nam zrównoleglenie obliczeń w tym przypadku. Tabela 5.2 zawiera czasy wygenerowania fraktala dla parametrów: - StartX = -2,1 - StartY = -1,3 - EndX = 1.0 - EndY = 1.3 Wielkość generowanego obrazka to 1280 na 748 pikseli. Tabela 5.2: Czas generowania fraktala Mandelbrota Wykonanie sekwencyjne Klasa Thread ParallelFX Procesor jednordzeniowy 1.1737804 1.4770110 1.2969678 Procesor dwurdzeniowy 3.8746301 11.5201821 03.7416921 Jednostka dwuprocesorowa Pierwsze spojrzenie na zamieszczone wyniki i od razu rzuca się w oczy trzy krotnie dłuższy czas wykonania operacji w oparciu o klasę Thread. Błąd nie leży tu jednak w narzędziu, a w sposobie jego wykorzystania. Listing 5.7 przedstawia kod, który tworzy nowy wątek dla każdego piksela szerokości generowanego obrazka. W omawianym przypadku, daje to ogromną liczbę 1280 wątków.

5.2 Zbiory Mandelbrota 65 Każdy z tych 1280 obiektów musi zostać stworzony i zainicjowany. Dodatkowo non stop kontekst procesora przełączany jest pomiędzy wątkami, co również odbija się na czasie wykonania całej operacji. Listing 5.9 przedstawia tą samą koncepcję rozwiązania, ale opartą o pule wątków - ThreadPool. Korzystamy w tym przypadku z pewnej ilości reużywalnych wątków, oszczędzając czas na ich tworzeniu i ograniczając ilość zmian kontekstu wykonania. Czasy generowania zbioru Mandelbrota z uwzględnieniem rozwiązania opartego o ThreadPool przedstawia tabela 5.3. private Image ThreadPoolGetBitmap ( double startx, double starty, double endx, double endy ) cs = GetColors (); object padlock = new object (); bitmap = new Bitmap ( this. picturebox. Width, this. picturebox. Height ); ParamDTO parameters = new ParamDTO (); parameters. XMin = startx ; parameters. YMin = starty ; parameters. XMax = endx ; parameters. YMax = endy ; parameters. DeltaX = ( endx - startx ) / this. picturebox. Width ; parameters. DeltaY = ( endy - starty ) / this. picturebox. Height ; Thread [] threads = new Thread [ picturebox. Width ]; ParameterizedThreadStart threadstart = new ParameterizedThreadStart ( ThreadAction ); for ( int i = 1; i < this. picturebox. Width ; i ++) ParamDTO nparams = ( ParamDTO ) parameters. Clone (); nparams. IterationCount = i; ThreadPool. QueueUserWorkItem ( new WaitCallback ( ThreadAction ), nparams ); WaitForThreads (); return ( Image ) bitmap ; Listing 5.9: Generowanie żuka Mandelbrota z wykorzystaniem ThreadPool Widzimy, że to rozwiązanie jest już konkurencyjne wobec korzystania z ParallelExtensions.

66 Rozdział 5. Testy Tabela 5.3: Czas generowania fraktala Mandelbrota (z uwzględnieniem ThreadPool) Wykonanie Klasa Klasa ParallelFX sekwencyjne Thread ThreadPool Procesor jednordzeniowy 1.1737804 1.4770110 1.3215774 1.2969678 Procesor dwurdzeniowy 3.8746301 11.5201821 3.9075880 03.7416921 Jednostka dwuprocesorowa 5.3 Filtry graficzne Ten test, a raczej zbiór testów ma za zadanie porównanie wydajności filtrów i efektów graficznych zaimplementowanych w oparciu o klasę Thread i Parallel Extensions. Wykorzystane zostały filtry będące składowymi dołączonej do niniejszej pracy aplikacji Parallel Image Effects i zostały już omówione w rozdziale jej poświęconym. Dlatego też, po opis poszczególnych funkcji odsyłam do rozdziału 4, a tu skupię się jedynie na otrzymanych wynikach. Wszystkie testy przeprowadzono na tym samym, przedstawionym niżej obrazku 1 (rys. 5.2) o rozmiarach 512x512. Rysunek 5.2: Obrazek wykorzystywany w testach filtrów i efektów graficznych 1 Obrazek pochodzi z kolekcji obrazków przykładowych dla zadań z przedmiotu Przetwarzanie Obrazu. Można go odnaleźć w internecie pod adresem http://ics.p.lodz.pl/~tomczyk/available/po/images/lenac.bmp