Biegala_Optymalizacj..

Transkrypt

Biegala_Optymalizacj..
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
I
5
Cel pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
Konwencje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
Podziękowania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
Część teoretyczna
9
2 Wielowątkowość
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.6
II
11
3.5.1
Klasa Thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.5.2
Współdzielenie danych . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.5.3
Klasa ThreadPool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
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
Część praktyczna
4 Parallel Image Effects
39
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 Moore1 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 do1
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 Reed2 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.51
- ASP 4.02
- Next-Generation Windows Services3
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 OmniVM4 , 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 .NET5
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
3.3
15
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.
Wersja
1.0
1.1
Data wydania
2002-02-13
2003-04-24
2.0
2005-11-07
3.0
2006-11-06
3.5
2007-11-19
4.0
2010-04-12
Tabela 3.1: Wersje .NET Framework
Opis
Pierwsza wersja .NET Framework
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)
Wprowadzenie klas generycznych, typów nullowalnych
(nullable types), klas partial, metod anonimowych,
rozszrzenie kontrolek ASP.NET
Wprowadzenie Windows Presentation Foundation(WPF),
Windows
Comunication
Foundation(WCF), Windows Workflow Foundation(WF).
Integracja z systemami Windows Vista i Windows
Server 2008
Dodanie wyrażeń lambda, extension methods, obsługi
AJAX do ASP.NET, LINQ
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
Opis
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
IsAlive
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
Nazwa
Aborted
AbortRequested
Background
Running
Stopped
StopRequested
Suspended
SuspendRequested
Unstarted
WaitSleepJoin
17
Tabela 3.4: Wartości enumeracji ThreadState
Opis
Wykonanie wątku zostało przerwane
Żądanie przerwania zostało wysłane, lecz sam
wątek wciąż pracuje
Wątek pracuje w tle
Wątek jest uruchomiony
Wykonanie wątku zostało zatrzymane
Żądanie zatrzymania wątku zostało wysłane
Wykonanie wątku zostało zawieszone7
Żądanie zawieszenia wątku zostało wysłane
Wątek został stworzony, lecz nie został jeszcze
uruchomiony
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 ()
{
P a r a m e t e r i z e dT h r e ad S t a rt methodRef = new Pa r am et r iz e dT hr e ad S ta rt ( 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 . En dCriticalRegion () ;
}
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
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
Nazwa
Enter
TryEnter
Exit
Wait
Pulse
PulseAll
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
Nazwa
GetAvailableThreads
GetMaxThreads
GetMinThreads
QueueUserWorkItem
SetMaxThreads
SetMinThreads
3.6: Podstawowe metody statyczne klasy ThreadPool
Opis
Zwraca aktualnie dostępną liczbę wątków
Zwraca maksymalną liczbę wątków jakie mogą
zostać utworzone w puli
Zwraca minimalną ilość wątków jakie muszą być
utworzone w puli
Zgłasza kod do wykonania w wątku z puli
Ustawia maksymalną ilość wątków w puli
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 Extensions8 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
C a n c e l l a t io n To ke n So ur c e tokenSource =
new C a n ce l la ti o nT o ke nS o ur c e () ;
// pobieramy obiekt żetonu
Can cellationToken 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 . I sC an cel at io nRe qu es ted )
{
// wykonujemy kod zapewniający integralność
// aplikacji ( o ile jest wymagany )
// i wyrzucamy poniższy wyjątek , informując
// CLR o przerwaniu zadania
throw new O p e r a t i o n C a n c e l e d E x c e p t i o n ( 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
C a n c e l l a t i o n To ke n So ur c e tSource1 = new C an ce l la t io nT o ke n So ur c e () ;
C a n c e l l a t i o n To ke n So ur c e tSource2 = new C an ce l la t io nT o ke n So ur c e () ;
C a n c e l l a t i o n To ke n So ur c e tSource3 = new C an ce l la t io nT o ke n So ur c e () ;
// tworzymy zbiorczy żeton
C a n c e l l a t i o n To ke n So ur c e linkedToken =
C a n c e l l a t io nT o ke n So ur c e . C re at e Li n ke dT o ke n So ur c e ( tSource1 . Token ,
tSource2 . Token ,
tSource3 . Token ) ;
// tak przygotowany obiekt , reaguje na wywołanie przerwania wątku
// z każdego ze źródeł tSource
Can cellat ionToken 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 ++)
{
P e r f o r mL o ng Ca l cu l at io n s ( numbers [ i ]) ;
}
foreach ( var num in number )
{
P e r f o r mL o ng Ca l cu l at io n s ( num ) ;
}
Parallel . For (0 , numbers . Lenght , ( i ) = >
{
P e r f or mL o ng Ca l cu l at io n s ( numbers [ i ]) ;
}) ;
Parallel . Foreach ( numbers , num = >
{
P e r f or mL o ng Ca l cu l at io n s ( 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 ;
}
P e r f o r m L o ng C al cu l at io n s ( 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 () ;
}
P e r f or mL o ng Ca l cu l at io n s ( 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
9
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
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
Opis
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
Nazwa
Created
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’
Nowa zakładka
Otwiera nową pustą zakładkę.
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) =
X
1
f (s, t)
mn − d (s,t)eS
(4.1)
xy
F (x, y) określa wartość piksela w punkcie x, y po przetworzeniu, Sxy 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.
P
(s,t)⊂Sxy
F (x, y) = P
f (s, t)Q+1
(s,t)⊂Sxy
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.
F (x, y) =
M
X
M
X
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
−1
h(, ) = 1
1
−1
−2
1
−1 1 .
1 • Maska zachód
1 1
1
h(, ) = 1 −2 −1
1 −1 −1
4.3
4.3.1
.
Budowa projektu
Technologie i narzędzia
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 , t r a n s f o r m e d I m a g e P i x e l F o r m a t ) ;
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
z0 = 0
n+1
= zn2 + p
(5.1)
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: |z1 | < 2
- dla przybliżenia 3: |z1 | < 2i|z2 | < 2
- ...
- dla przybliżenia n: |z1 | < 2i|z2 | < 2, ..., |zn−1 | < 2
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 ];
P a r a m e t e r i z e dT h r e ad S t ar t threadStart = new P ar a m e te r i z ed T h r ea d S t ar t ( 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
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 ;
}
63
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 ];
P a r a m e t e r i z e dT h r e ad S t ar t threadStart = new P ar a m e te r i z ed T h r ea d S t ar t ( 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 obrazku1 (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
5.3 Filtry graficzne
67
Zmiana jasności
Parametry:
- Zwiększamy jasność o 1
Tabela 5.4: Czas wykonanania zmiany jasności
Wykonanie
Klasa
ParallelFX
sekwencyjne
Thread
Procesor jednordzeniowy
0.0139329
0.1109539
0.0289491
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
Operator Rozenfelda
Parametry:
- Parametr operatora = 3
Tabela 5.5: Czas operacji wykorzystania operatora Rozenfelda
Wykonanie
Klasa
ParallelFX
sekwencyjne
Thread
Procesor jednordzeniowy
0.0575603
0.1093908
0.0602777
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
Filtr ze średnią kontrharmoniczną
Parametry:
- Maska = 3
- Rząd filtru = 1
Tabela 5.6: Czas wykonania operacji filtracji kontrharmonicznej
Wykonanie
Klasa
ParallelFX
sekwencyjne
Thread
Procesor jednordzeniowy
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
14.9030708
16.9904614
22.1078299
68
Rozdział 5 . Testy
Filtr medianowy
Parametry:
- Maska = 3
Tabela 5.7: Czas wykonania operacji filtracji medianowej
Wykonanie
Klasa
ParallelFX
sekwencyjne
Thread
Procesor jednordzeniowy
4.6224368
8.8952400
5.6256060
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
Filtr alfa-obcięty
Parametry:
- Maska = 3
- Ilość pikseli do usunięcia = 1
Tabela 5.8: Czas wykonania filtru alfa-obciętego
Wykonanie
Klasa
ParallelFX
sekwencyjne
Thread
Procesor jednordzeniowy
5.0012161
6.3468302
6.2321024
Wykonanie
sekwencyjne
Klasa
Thread
ParallelFX
14.4206698
15.2108073
14.5250443
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
Splot
Parametry:
- Maska zachód
Procesor jednordzeniowy
Procesor dwurdzeniowy
Jednostka dwuprocesorowa
5.3 Filtry graficzne
Podsumowanie
Tu będzie podsumowanie testów
69
70
Rozdział 5 . Testy
Bibliografia
[1] http://pl.wikipedia.org/wiki/Gordon_E._Moore
[2] http://pl.wikipedia.org/wiki/Prawo_Moore’a
[3] http://research.microsoft.com/en-us/people/reed/
[4] http://www.bsdg.org/2006/06/brief-history-of-c-and-c-langauge.html
[5] http://www.c2.com/cgi/wiki?HistoryOfCsharp
[6] http://jameskovacs.com/2007/09/07/cnet-history-lesson/
[7] http://blogs.msdn.com/b/patrick_dussud/archive/2006/11/21/
how-it-all-started-aka-the-birth-of-the-clr.aspx
[8] http://geekswithblogs.net/sdorman/archive/2006/11/27/99231.aspx
[9] http://msdn.microsoft.com/pl-pl/netframework/cc511285.aspx
[10] http://en.wikipedia.org/wiki/.NET_Framework
[11] http://msdn.microsoft.com/en-us/library/system.threading.monitor.aspx
[12] http://msdn.microsoft.com/en-us/library/hf5de04k.aspx
[13] http://msdn.microsoft.com/en-us/library/system.threading.threadpool.aspx
[14] http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx
[15] Tony Northrup: Microsoft .NET Framework 2.0 Foundation Training Kit
Microsoft Press (2006)
[16] http://www.pi.zarz.agh.edu.pl/tematy/ea/labor/ea_zad1.html
[17] Zdzisław Płoski: Słownik Encyklopedyczy - Informatyka
Wydawnictwo Europa (1999)
[18] http://www.algorytm.org/fraktale/zbior-mandelbrota.html
[19] http://portalwiedzy.onet.pl/6822,,,,macierz,haslo.html
72
BIBLIOGRAFIA
Spis rysunków
1.1
Ilustracja prawa Moora[2] . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
3.1
Struktura .NET Framework 2.0[9] . . . . . . . . . . . . . . . . . . . . . . .
14
3.2
Ilustracja sekcji krytycznych[15] . . . . . . . . . . . . . . . . . . . . . . . . .
20
3.3
Ilustracja pracy planisty zadań . . . . . . . . . . . . . . . . . . . . . . . . .
28
4.1
Okno główne programu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
4.2
Okno dialogowe Jasność/Kontrast . . . . . . . . . . . . . . . . . . . . . . .
44
4.3
Okno dialogowe obsługi filtru alfa-obciętego . . . . . . . . . . . . . . . . .
45
4.4
Okno dialogowe obsługi filtru średniej kontrharmonicznej . . . . . . . . .
46
4.5
Okno dialogowe wywołania filtru medianowego
. . . . . . . . . . . . . . .
47
4.6
Okno dialogowe operatora Rozenfelda . . . . . . . . . . . . . . . . . . . . .
47
4.7
Okno dialogowe operacji wydobywania szczegółów tła . . . . . . . . . . .
48
4.8
Diagram klas implementujących filtry i operatory graficzne . . . . . . . .
51
5.1
Przykładowy fraktal Mandelbrota . . . . . . . . . . . . . . . . . . . . . . .
60
5.2
Obrazek wykorzystywany w testach filtrów i efektów graficznych . . . . .
66
74
SPIS RYSUNKÓW
Spis tabel
3.1
Wersje .NET Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
3.2
Podstawowe właściwości obiektów klasy Thread . . . . . . . . . . . . . . .
16
3.3
Podstawowe metody obiektów klasy Thread . . . . . . . . . . . . . . . . .
16
3.4
Wartości enumeracji ThreadState . . . . . . . . . . . . . . . . . . . . . . . .
17
3.5
Podstawowe metody obiektów klasy Monitor . . . . . . . . . . . . . . . . .
24
3.6
Podstawowe metody statyczne klasy ThreadPool . . . . . . . . . . . . . .
26
3.7
Wartości TaskCreationOptions . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.8
Wartości TaskStatus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
5.1
Czas odwiedzenia każdej gałęzi drzewa . . . . . . . . . . . . . . . . . . . .
59
5.2
Czas generowania fraktala Mandelbrota . . . . . . . . . . . . . . . . . . . .
64
5.3
Czas generowania fraktala Mandelbrota (z uwzględnieniem ThreadPool)
66
5.4
Czas wykonanania zmiany jasności . . . . . . . . . . . . . . . . . . . . . . .
67
5.5
Czas operacji wykorzystania operatora Rozenfelda . . . . . . . . . . . . .
67
5.6
Czas wykonania operacji filtracji kontrharmonicznej . . . . . . . . . . . .
67
5.7
Czas wykonania operacji filtracji medianowej . . . . . . . . . . . . . . . . .
68
5.8
Czas wykonania filtru alfa-obciętego . . . . . . . . . . . . . . . . . . . . . .
68
76
SPIS TABEL
Listingi
3.1
Przykład kodu MSIL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
3.2
Przykładowy kod metody wykonywanej w wątku pobocznym . . . . . . .
17
3.3
Przykładowy kod tworzący wątek . . . . . . . . . . . . . . . . . . . . . . . .
17
3.4
Tworzenie i uruchamianie wielu wątków . . . . . . . . . . . . . . . . . . . .
17
3.5
Uruchamianie wątku z parametrem . . . . . . . . . . . . . . . . . . . . . .
18
3.6
Wykorzystanie Thread.Join . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
3.7
Przykład sekcji krytycznej . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
3.8
Przykładowe odwołanie do zmiennej - jeden wątek . . . . . . . . . . . . .
20
3.9
Przykładowe odwołanie do zmiennej - wiele wątków . . . . . . . . . . . .
21
3.10 Kod pośredni dla operacji przypisania . . . . . . . . . . . . . . . . . . . . .
22
3.11 Odwołanie do zmiennej z uwzględnieniem wielu wątków . . . . . . . . . .
23
3.12 Odwołanie do zmiennej z użyciem klasy Monitor . . . . . . . . . . . . . .
24
3.13 Przykład użycia metody Monitor.TryEnter . . . . . . . . . . . . . . . . . .
25
3.14 Przykład użycia klasy ThreadPool . . . . . . . . . . . . . . . . . . . . . . .
25
3.15 Przykłady tworzenia nowych zadań(Task) . . . . . . . . . . . . . . . . . . .
29
3.16 Tworzenia zadań(Task) z parametrem . . . . . . . . . . . . . . . . . . . . .
30
3.17 Przykład pobrania wyniku działania zadania . . . . . . . . . . . . . . . . .
31
3.18 Anulowanie zadania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
32
3.19 Tworzenie złożonych obiektów CancellationToken . . . . . . . . . . . . . .
33
3.20 Przykład użycia klasy Lazy . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
3.21 Przykład użycia pętli Parallel.For
. . . . . . . . . . . . . . . . . . . . . . .
35
3.22 Przykład przerwania pętli Foreach . . . . . . . . . . . . . . . . . . . . . . .
36
3.23 Przykład przerwania pętli Parallel.Foreach . . . . . . . . . . . . . . . . . .
36
4.1
Implementacja metody WaitForThreads . . . . . . . . . . . . . . . . . . . .
52
4.2
Przykład operacji na bitmapie ulokowanej w pamięci operacyjnej . . . .
52
5.1
Implementacja drzewa binarnego . . . . . . . . . . . . . . . . . . . . . . . .
55
5.2
Klasa TreeTravel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
56
5.3
Implementacja WalkTree za pomocą rekurencji . . . . . . . . . . . . . . .
57
5.4
Implmenetacja WalkTree z wykorzystaniem klasy Thread . . . . . . . . .
57
5.5
Implementacja WalkTree z użyciem obiektów Task . . . . . . . . . . . . .
58
5.6
Sekwencyjne generowanie ”żuka Mandelbrota” . . . . . . . . . . . . . . .
59
5.7
Generowanie ”żuka Mandelbrota” z wykorzystaniem klasy Thread . . .
61
5.8
Generowanie ”żuka Mandelbrota” z wykorzystaniem ParallelFX . . . . .
62
78
LISTINGI
5.9
Generowanie ”żuka Mandelbrota” z wykorzystaniem ThreadPool . . . .
65

Podobne dokumenty