Programowanie C/C++ Janusz Ganczarski Programowanie wielowątkowe w WIN32 Obserwacje kierunku rozwoju współczesnych komputerów PC, upowszechnienie procesorów dwurdzeniowych i bliska perspektywa pojawienia się procesorów o jeszcze większej ilości rdzeni oznacza dla programistów konieczność wyboru technik, które umożliwią pełne wykorzystanie nowych możliwości. Jedną w możliwości jest użycie w programach wątków, które odpowiednio zastosowane pozwolą zarówno zwiększyć wydajność aplikacji jak i poprawić komfort użytkownika. Wielowątkowość Większość współczesnych systemów operacyjnych jest wielozadaniowa tzn. pozwala na równoczesne (współbieżne) uruchamianie wielu procesów. Oczywiście w przypadku, gdy system ma do dyspozycji mniej procesorów niż procesów wielozadaniowość realizowana jest poprzez odpowiedni podział czasu pracy procesora (procesorów). Zadaniem kolejkowania dostępu procesów do procesora zajmuje się moduł systemu operacyjnego nazywany planistą (ang. scheduler). Natomiast wielowątkowość to cecha systemu operacyjnego pozwalająca na wykonywanie w ramach jednego procesu wielu równocześnie (współbieżnie) wielu wątków (ang. threads). Podobnie jak w przypadku wielozadaniowości, współbieżność wykonywania wątków realizowana jest poprzez odpowiedni podział czasu pracy procesora. Wątek a proces Procesem określamy zazwyczaj wykonywany program w skład, którego wchodzą: kod programu, licznik rozkazów, stos procesu i sekcja danych. Wątek wchodzący w skład danego procesu dzieli wraz z innymi równorzędnymi wątkami kod programu, sekcję danych oraz zasoby systemowe. Natomiast wątek posiada własny licznik rozkazów, zbiór rejestrów procesora i stos. W publikacjach dotyczących programowania współbieżnego można spotkać się także określeniem wątku jako procesu lekkiego LWP (ang. lightweight process), w odróżnieniu od tradycyjnego procesu nazywanego procesem ciężkim. Implementacje wątków Wątki mogą być różnie zaimplementowane w systemie operacyjnym. Wątki poziomu użytkownika tworzone są Autor jest matematykiem i informatykiem. Interesuje się programowaniem wieloplatformowym z użyciem języków C i C++ oraz algorytmami w grafi ce komputerowej. Kontakt z autorem: JanuszG@enter.net.pl za pomocą funkcji bibliotecznych. W takim przypadku przełączanie między wątkami nie wymaga działania systemu operacyjnego. Zaletą takiego rozwiązania jest duża szybkość przełączania pomiędzy wątkami i związana z tym duża wydajność nawet przy znacznym obciążeniu. Efektywność takiego modelu zależy od możliwości systemu operacyjnego. Może się bowiem okazać, że proces wielowątkowy otrzyma tyle samo czasu pracy procesora co proces jednowątkowy. W przypadku jednowątkowego jądra systemu każde odwołanie wątku poziomu użytkownika do systemu spowoduje wstrzymanie całego procesu. Innym sposobem implementacji wątków jest ich obsługa przez jądro systemu operacyjnego wątki jądra. Rozwiązanie to pozwala systemowi operacyjnemu na właściwy przydział czasu pracy procesora (procesorów) ale może wiązać się z wolniejszą obsługą przełączania wątków. Ostatnim spotykaną metodą implementacji wątków są tzw. wątki mieszane łączące w sobie techniki wątków poziomu użytkownika i wątków jądra. Modele wielowątkowości Można wyróżnić trzy zasadnicze modele wielowątkowości. W modelu wiele na jeden (ang. many-to-one) wiele wątków poziomu użytkownika odwzorowywanych jest na jeden wątek jądra. Ten model wielowątkowości stosowany jest w systemach operacyjnych nie posiadających mechanizmów wątków jądra. W drugim modelu jeden na jeden każdy wątek poziomu użytkownika jest odwzorowywany na jeden wątek jądra. Ostatnim stosowanym modelem jest wiele na wiele (ang. manyto-many), w którym wiele wątków poziomu użytkownika może być odwzorowanych na wiele wątków jądra. Wady i zalety wątków Podstawową zaletą wątków jest dzielenie zasobów systemowych przydzielonych procesowi. Ułatwia to znacznie komunikację między wątkami, która w odróżnieniu od komunikacji pomiędzy procesami nie musi korzystać z kosztownych mechanizmów IPC oraz pozwala na oszczędniejsze wykorzystanie zasobów. W architekturach wieloprocesorowych współbieżne uruchamianie wątków pozwala na zwiększenie wydajności programu. Niestety wątki posiadają również wadę wynikającą ze współdzielenia zasobów jeden błędnie działający wątek może zagrozić działaniu całego procesu. Wątki wymagają także synchronizacji, która jest cechą specyficzną dla danego systemu operacyjnego. Synchronizacja związana jest z dodatkowym narzutem czasu i zwiększeniem rozmiaru programu, a także powoduje dodatkowe problemy na etapie tworzenia i testowania programu. 32 www.sdjournal.org Software Developer s Journal 4/2006
Programowanie wielowątkowe w WIN32 Synchronizacja wątków Dopóki wątki wykonują całkowicie od siebie niezależne zadania oraz nie korzystają ze wspólnych zasobów w innych sposób niż ich odczytywanie problemy związane z ich synchronizacją nie występują. Jednak każdy inny przypadek korzystania ze wspólnych zasobów wymaga realizacji wzajemnego wykluczania dostępu do zasobów. Techniki to realizujące nazywane są ogólnie mechanizmami synchronizacji. Literatura dotycząca programowania współbieżnego i rozproszonego opisuje kilka tzw. klasycznych problemów współbieżności. Choć autorzy przestawiając te zagadnienia posługują się procesami, to synchronizacja wątków napotyka na dokładnie takie same problemy. Podstawowe mechanizmy synchronizacji Literatura tematu opisuje kilka podstawowych mechanizmów wzajemnego wykluczania umożliwiających synchronizację procesów (wątków). Do najprostszych rozwiązań można zaliczyć instrukcje niepodzielne, zwane także operacjami atomowymi. Są to instrukcje, co do których istnieje gwarancja, że w trakcie ich wykonywania nie nastąpi przerwanie wykonywania wątku przez inny wątek. Aby zrozumieć znaczenie takiej gwarancji trzeba pamiętać, że nawet pojedyncza instrukcja w języku programowania wysokiego poziomu może zostać przetłumaczona na wiele instrukcji procesora. A to oznacza, że w tym czasie inny wątek może np. odczytać błędną wartość właśnie modyfikowanej zmiennej globalnej. Rozwinięciem powyższego mechanizmu są sekcje krytyczne, które nie pozwalają na przerwanie wykonania określonego fragmentu kodu programu. Operacje atomowe i sekcje krytyczne można zaliczyć do niskopoziomowych mechanizmów synchronizacji wątków. Na wyższym poziomie abstrakcji znajdują się mechanizmy nazywane semaforami. Semafor to liczba całkowita, na której poza ustaleniem stanu początkowego, można wykonać operacje podniesienia i opuszczenia (zmniejszenie liczby). Oczywiście obie operacje muszą być niepodzielne. Semafor można zdefiniować jako ogólny, gdzie podniesienie semafora powoduje, że jeden z oczekujących wątków zostanie wznowiony, a operacja opuszczenia nie pozwoli wątkowi ją wykonującemu na dalsze działanie o ile wartość semafora wynosiłaby 0. W odróżnieniu od semafora ogólnego semafor binarny zawiera tylko dwie wartości 0 lub 1. Należy pamiętać, że praktyczne rozwiązania mechanizmów synchronizacji wątków zależą ściśle od możliwości oferowanych przez system operacyjny lub zastosowaną bibliotekę do obsługi wątków. Zakleszczenia Najczęściej spotykanym problemem związanym z synchronizacją wątków jest zjawisko zakleszczenia (ang. deadlock), opisywane w literaturze także pod określeniem blokady, zastoju lub martwego punktu. Z zakleszczeniem zbioru wątków mamy miejsce wówczas, gdy każdy z wątków z tego zbioru oczekuje na zdarzenie, które może być spowodowane tylko przez inny wątek z tego zbioru. Detekcję zjawiska zakleszczania utrudnia specyfika współbieżności, bowiem blokada wątków nie musi wystąpić przy każdym uruchomieniu programu. Najprostszy przykład zakleszczenia, tzw. zastój meksykański, opisują dwa wątki, które do swojego działania potrzebują dwóch różnych zasobów. Oczywiście oba zasoby zabezpieczone są odrębnymi sekcjami krytycznymi lub przy pomocy innych mechanizmów wzajemnego wykluczania. Pierwszy wątek najpierw próbuje korzystać z pierwszego zasobu, a następnie z drugiego. Natomiast drugi wątek najpierw próbuje korzystać z drugiego zasobu, a następnie z pierwszego. W efekcie dochodzi do blokady, bowiem żaden z wątków nie może wejść do obszaru chronionego. Do analizy systemów mogących wejść w stan zakleszczenia stosuje się grafy przydziału zasobów. Wierzchołki grafu stanowią wątki {W1, W2,..., Wn} i zasoby {Z1, Z2,..., Zm}. Graf posiada dwa rodzaje krawędzi. Krawędź zamówienia Wi Zj oznacza, że wątek Wi zamówił zasób Zj. Natomiast krawędź przydziału Zj Wi oznacza, że jeden egzemplarz zasobu Zj został przydzielony wątkowi Wi. Oczywiście liczba krawędzi przydziału nie może przekroczyć ilości egzemplarzy zasobu Zj. Jeżeli w grafie przydziału zasobów nie ma cykli, to w programie nie ma zakleszczeń. Wystąpienie cyklu jest warunkiem koniecznym do wystąpienia zakleszczenia, ale niestety nie jest to warunek wystarczający. Wystąpienie cyklu będzie jednocześnie warunkiem wystarczającym, jeżeli wszystkie zasoby mają jeden egzemplarz. W przeciwnym wypadku samo wystąpienie cyklu nie będzie warunkiem wystarczającym. Nie ma jednej uniwersalnej metody zapobiegania zakleszczaniom. Podstawową techniką jest nałożenie ograniczeń na sposób dostępu do zasobów, tak aby nie był spełniony warunek konieczny do wystąpienia zakleszczenia. Bardziej kosztowne techniki związane są z bieżącą analizą przydziału zasobów i detekcją czy nie występuje cykl w grafie przydziału zasobów. Można także stosować algorytm bankiera, którego idea sprowadza się do zbadania bezpieczeństwa operacji przydziału zasobu przed dokonaniem tego przydziału. Inny stosowany w praktyce sposób, to przyjęcie dopuszczalności zakleszczenia, przy czym w razie jego wykrycia jeden lub cała grupa z zablokowanych wątków są usuwane. Klasyczne problemy współbieżności Jednym z klasycznych problemów współbieżności jest problem producenta i konsumenta. Polega on na synchronizacji dwóch wątków: producenta, który systematycznie przygotowuje określoną porcję informacji oraz producenta, który porcje przygotowane przez producenta cyklicznie pobiera i przetwarza. Problem z synchronizacją pojawia się jeżeli producent w danej chwili nie przekazać wytworzonej porcji informacji lub, gdy konsument nie może pobrać przygotowanej przez producenta informacji. Zarówno w jednym jak i w drugim przypadku jeden z wątków musi czekać na drugi. Kolejnym klasycznym problemem współbieżności jest problem czytelników i pisarzy, który polega na synchronizacji dwóch grup cyklicznych procesów konkurujących o dostęp do wspólnej czytelni. Wątek czytelnik periodycznie odczytuje dane znajdujące się w czytelni. Wątek pisarz także co jakiś czas potrzebuje dostępu do czytelni ale z uwagi na dokonywanie zapisu informacji musi przebywać w czytelni sam. Takiego ograniczenia nie mają wątki czytelników, które mogą przebywać w czytelni grupowo. Rozważaniu podlega także wariant tego problemu z ograniczoną ilością miejsc w czytelni dla czytelników. Trzecim klasycznym problemem współbieżności jest problem pięciu filozofów. Zagadnienia polega na synchronizacji działań pięciu wątków filozofów, którzy siedzą przy okrągłym stole i myślą. Co jakiś czas każdy filozof staje się głodny i musi się pożywić. Do tego celu każdy z filozofów ma przed sobą talerz, a pomiędzy dwoma talerzami leży jeden widelec. Ponieważ jednak spożywana potrawa wymaga dwóch widelców filozof przystępując do spożycia posiłku uniemożliwia jedzenie swoim sąsiadom. Software Developer s Journal 4/2006 www.sdjournal.org 33
Programowanie C/C++ Problem zakłada skończoną ilość czasu potrzebnego na jedzenie i wymaga aby każdy filozof mógł najeść się do syta. Wielowątkowość w WIN32 W systemach z rodziny Windows zastosowano model wielowątkowości jeden na jeden (ang. one-to-one). Wątki są bezpośrednio obsługiwane i szeregowane przez jądro systemu. Każdy proces posiada jeden wątek tworzony automatycznie. Pozostałe wątki tworzone są jawnie trakcie działania procesu. API systemów WIN32 udostępnia 5 metod synchronizacji wątków. Są to: sekcje krytyczne, muteksy, semafory, zdarzenia i zegary oczekujące. Pierwsza metoda działa wyłącznie w granicach procesu, pozostałe mogą być używane do synchronizacji wątków na poziomie różnych procesów. Programista ma także do dyspozycji operacje atomowe, które działają na poziomie pojedynczego procesu. Prezentowane poniżej przykładowe programy kompilowano przy użyciu następujących kompilatorów: MinGW (GCC 3.4.4), Borland C++ Compiler 5.5 oraz Microsoft Visual C++ 2005 Express Edition. Niezależnie od użytego kompilatora trzeba pamiętać o wybraniu opcji kompilacji programu wielowątkowego, co spowoduje użycie przez kompilator wersji bibliotek RTL przystosowanych do pracy wielowątkowej. Tworzenie wątku Nowy wątek tworzy funkcja CreateThread, która posiada następujące parametry: lpthreadattributes wskaźnik na strukturę SECURITY _ ATTRIBUTES określającej atrybuty zabezpieczeń i dostępu do nowego wątku; w większości przypadków wystarczy przyjęcie ustawień domyślnych i podanie wartości NULL, dwstacksize początkowa (lub zalecana) wielkość stosu nowego wątku, podanie wartości 0 spowoduje przyjęcie przez system domyślnej wielkości stosu; w razie potrzeby wielkość stosu jest automatycznie powiększana przez system, lpstartaddress wskaźnik na funkcję wątku, lpparameter wskaźnik na parametr przekazywany w funkcji wątku, dwcreationflags znacznik kontrolujący proces tworzenia wątku; wartość 0 spowoduje natychmiastowe uruchomienie funkcji wątku; flaga CREATE _ SUSPENDED spowoduje zawieszenie wykonywania wątku do czasu wywołania funkcji Resume- Theread; flaga STACK _ SIZE _ IS _ A _ PARAM _ IS _ A _ RESERVA- TION oznacza, że parametr dwstacksize zawiera początkową wielkość stosu, w przeciwnym wypadku parametr ten zawiera zalecaną wielkość stosu, lpthreadid wskaźnik na zmienną zawierającą unikatowy identyfikator wątku. Jeżeli utworzenie nowego wątku przebiegło pomyślnie funkcja CreateThread zwraca uchwyt wątku. W przypadku błędu zwrócona zostaje wartość NULL. Funkcja wątku musi mieć postać: DWORD WINAPI ThreadProc (LPVOID lpparameter). Parametr lpparameter jest przekazywany przez funkcję tworzącą nowy wątek. W razie potrzeby można uzyskać uchwyt bieżącego wątku przy użyciu funkcji GetCurrentThread. W szczególności może to być uchwyt do głównego wątku procesu. Co ważne tak uzyskany uchwyt, nazywany w specyfi kacji API pseudo uchwytem, nie musi być zwalniany poprzez wywołanie funkcji CloseHandle. Do utworzenia kopii uchwytu wątku należy użyć funkcji DuplicateHandle. API umożliwia także pobranie identyfi katora bieżącego wątku służy do tego funkcja Get- CurrentThreadId. Alternatywnie do tworzenia wątku można wykorzystać funkcję _ beginthread lub _ beginthreadex z biblioteki RTL kompilatora Visual C++. Funkcje te udostępniają również inne kompilatory, w tym Borland C++ i MinGW. Prototypy tych funkcji zawarte są w pliku nagłówkowym process.h. Zakończenie wątku Wątek można zakończyć na trzy sposoby: zakończenie funkcji wątku, do której wskaźnik zawarty jest w parametrze lpstartaddress funkcji CreateThread; jest to zalecana metoda zakończenia wątku w programie napisanym w języku C++; zakończenie wątku w ten sposób powoduje niejawne wywołanie funkcji ExitThread, wywołanie ExitThread w funkcji wątku; jest to zalecana metoda zakończenia wątku w programie napisanym w języku C; parametr tej funkcji określa wartość zwróconą przez funkcję wątku, wywołanie funkcji TerminateThread, która pozwala na zakończenie wątku w dowolnie wybranym momencie; niestety oznacza to także potencjalne niebezpieczeństwo nie zwolnienia zasobów przydzielonych przez wątek. Pobranie wartości zwróconej przez funkcję wątku umożliwia funkcja GetExitCodeThread. Uchwyt wątku, podobnie jak uchwyty do innych zasobów w systemach WIN32, należy zwolnić przy użyciu funkcji CloseHandle. Także do zakończenia wątku biblioteka RTL kompilatora Visual C++ udostępnia własne funkcje: _endthread i _ endthreadex. Pierwszy przykładowy program (Listing nr 1) tworzy dwa wątki które manifestują swoją obecność stosownymi komunikatami. Po utworzeniu wątków program czeka jedną sekundę (funkcja Sleep), aby wątki zakończyły swoją pracę przed zakończeniem pracy programu. Listing 1. Prosty program tworzący dwa wątki #include <windows.h> #include <stdio.h> DWORD Thread1 (LPVOID /*arg*/) { printf ("Pierwszy wątek\n"); DWORD Thread2 (LPVOID /*arg*/) { printf ("Drugi wątek\n"); int main () { DWORD ID1,ID2; CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)Thread1,NULL, 0,&ID1); CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)Thread2,NULL, 0,&ID2); Sleep (1000); printf ("Zakończono wszystkie wątki\n"); return 0; } 34 www.sdjournal.org Software Developer s Journal 4/2006
Programowanie wielowątkowe w WIN32 Operacje atomowe API systemów operacyjnych z rodziny Windows dostarcza operacji atomowych dla zmiennych 32 i 64 bitowych. Są to następujące funkcje: InterlockedExchange, InterlockedExchange64 przypisanie zmiennej wybranej wartości, InterlockedIncrement, InterlockedIncrement64 zwiększenie wartości zmiennej o 1, InterlockedDecrement, InterlockedDecrement64 zmniejszenie wartości zmiennej o 1, InterlockedExchangeAdd, InterlockedExchangeAdd64 dodanie do zmiennej dowolnej wartości całkowitej, InterlockedComapreExchange, InterlockedExchangeAdd64 porównanie wartości dwóch zmiennych. Powyższych funkcji można użyć do operacji na zmiennych o mniejszej ilości bitów niż 32 lub 64. Jednak warunkiem przewidywalności otrzymanego wyniku jest wyrównanie tych zmiennych do granicy odpowiednio 4 i 8 bajtów. Do operacji atomowych na wskaźnikach służy odrębny zestaw funkcji: InterlockedExchangePointer zamiana wartości dwóch wskaźników, InterlockedCompareExchangePointer porównanie wartości dwóch wskaźników. Podobnie jak w przypadku operacji na zmiennych całkowitych wskaźniki muszą być wyrównane do 4 bajtów w systemach 32 bitowych i 8 bajtów w systemach 64 bitowych. Korzystanie z operacji atomowych zapewnia poprawne wykonywanie operacji arytmetycznych na zmiennych liczbowych i operacji na wskaźnikach. Trzeba jednak pamiętać, że po wykonaniu wybranej operacji atomowej nie ma żadnej pewności, że inny współbieżnie działający wątek nie zmodyfikuje danej zmiennej lub wskaźnika. Ważną cechą operacji atomowych jest ograniczenie ich zasięgu do jednego procesu. Wpływa to oczywiście na stosunkowo wysoką szybkość ich działania. Popatrzmy przykładowy program przedstawiony na Listingu 2. Dwa równolegle działające wątki wykonują elementarne operacje arytmetyczne na zmiennej globalnej a. Pierwszy wątek milion razy zwiększa wartość a o 1; drugi wątek milion razy zmniejsza wartość zmiennej a o 1. Oczywiście sekwencyjne wykonanie obu funkcji nie zmieni początkowej wartości zmiennej a, która wynosi 0. Ale wykonanie współbieżne może dać zaskakujące i oczywiście niepoprawne rezultaty. Dopiero użycie obu funkcji w wersjach wykorzystujących operacje atomowe daje poprawny wynik końcowy, co oczywiście okupione jest zwiększeniem czasu obliczeń. W programie wykorzystano funkcję oczekującą WaitForSingleObject, której zadaniem jest oczekiwanie na przejście wątku w stan sygnalizowany, czyli na zakończenie pracy funkcji wątku. Funkcje oczekujące Zadaniem funkcji oczekujących (ang. wait functions) jest oczekiwanie na przejście obiektu w stan sygnalizowany. Mechanizm ten obejmuje nie tylko wątki ale także szereg innych rodzajów obiektów, w tym opisywane dalej muteksy, semafory, zdarzenia i zegary oczekujące. Funkcje oczekujące można podzielić na dwie grupy. Pierwsza obsługuje pojedyncze obiekty, druga dowolnie wybrane grupy obiektów. Listing 2. Test operacji na zmiennej globalnej z zastosowaniem operacji atomowych i bez ich użycia #include <windows.h> #include <stdio.h> #include <time.h> signed long int a = 0; DWORD Thread1 (LPVOID /*arg*/) { for (int i = 0; i < 1000000; i++) a++; DWORD Thread2 (LPVOID /*arg*/) { for (int i = 0; i < 1000000; i++) a--; DWORD Thread1Save (LPVOID /*arg*/) { for (int i = 0; i < 1000000; i++) InterlockedIncrement (&a); DWORD Thread2Save (LPVOID /*arg*/) { for (int i = 0; i < 1000000; i++) InterlockedDecrement (&a); int main () { DWORD ID1,ID2; HANDLE h1,h2; long start = clock (); h1 = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)Thread1, NULL,0,&ID1); h2 = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE)Thread2, NULL,0,&ID2); WaitForSingleObject (h1,infinite); WaitForSingleObject (h2,infinite); CloseHandle (h1); CloseHandle (h2); printf ("Test bez operacji atomowych\n"); printf ("Zmienna: %i\n",a); printf ("Czas wykonania: %f\n",(float)(clock ()-start) /CLOCKS_PER_SEC); a = 0; start = clock (); h1 = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE) Thread1Save,NULL,0,&ID1); h2 = CreateThread (NULL,0,(LPTHREAD_START_ROUTINE) Thread2Save,NULL,0,&ID2); WaitForSingleObject (h1,infinite); WaitForSingleObject (h2,infinite); CloseHandle (h1); CloseHandle (h2); printf ("\ntest z operacjami atomowymi\n"); printf ("Zmienna: %i\n",a); printf ("Czas wykonania: %f\n",(float)(clock ()-start) /CLOCKS_PER_SEC); return 0; } Software Developer s Journal 4/2006 www.sdjournal.org 35
Programowanie C/C++ Oczekiwanie, aż pojedynczy obiekt znajdzie się w stanie sygnalizowanym, realizuje funkcja WaitForSingleObject, która posiada dwa argumenty: hhandle uchwyt na badany obiekt, dwmilliseconds czas oczekiwania w milisekundach; podanie wartości 0 powoduje wyłącznie sprawdzenie bieżącego statusu obiektu, natomiast użycie stałej INFINITE spowoduje oczekiwanie aż do przejścia obiektu w stan zasygnalizowany. W sytuacji, gdy oczekiwanie na przejście obiektu w stan sygnalizowany zakończyło się sukcesem funkcja WaitForSingleObject zwraca wartość WAIT _ OBJECT _ 0. Jeżeli po upływie czasu oczekiwania obiekt nie przeszedł w stan zasygnalizowany, funkcja zwróci wartość WAIT _ TIMEOUT. Wystąpienie jakiegokolwiek błędu podczas wykonywania funkcji spowoduje zwrócenie wartości WAIT _ FAILED. W przypadku, gdy badanym obiektem jest muteks funkcja WaitForSingleObject może także zwrócić wartość WAIT _ ABAN- DONED. Oznacza to, że jakiś wątek stał się właścicielem muteksu i kończąc swoje zadanie nie zwolnił go. W tej sytuacji wątek, który odebrał tę informację staje się nowym właścicielem muteksu. W przypadku, gdy oczekujemy na przejście w stan sygnalizowany wielu wątków lub innych obiektów można użyć funkcji Wait- ForMultipleObjects, która posiada następujące parametry: ncount wielkość tablicy przekazywanej w parametrze lphandles; wielkość maksymalną określa stała MAXIMUM _ WA- IT _ OBJECTS, lphandles wskaźnik na tablicę z uchwytami do badanych obiektów; tablica może zawierać uchwyty do obiektów różnego typu, bwaitall wartość TRUE oznacza, że funkcja będzie oczekiwała przejścia w stan sygnalizowany wszystkich badanych obiektów; wartość FALSE oznacza, że wystarczy aby jeden z badanych obiektów przeszedł w stan sygnalizowany, dwmilliseconds - czas oczekiwania w milisekundach; podanie wartości 0 powoduje wyłącznie sprawdzenie bieżących statusów obiektów, natomiast użycie stałej INFINITE spowoduje oczekiwanie aż do przejścia wszystkich lub jednego z obiektów w stan zasygnalizowany. Funkcja WaitForMultipleObjects zwraca następujące wartości lub zakresy wartości: od WAIT _ OBJECT _ 0 do jeżeli parametr bwaitall ma wartość TRUE to znaczy, że wszystkie badane obiekty są w stanie sygnalizowanym; jeżeli parametr bwaitall ma wartość FAL- SE, to zwrócona wartość pomniejszona o stałą WAIT _ OB- JECT _ 0 określa, który z obiektów wskazywanych w tablicy lphandles przeszedł w stan zasygnalizowany, od WAIT _ ABANDONED _ 0 do (WAIT _ ABANDONED _ 0 + ncount 1) jeżeli parametr bwaitall ma wartość TRUE to znaczy, że wszystkie badane obiekty są w stanie sygnalizowanym, ale co najmniej jeden stał się właścicielem muteksu i kończąc działanie nie zwolnił go; jeżeli parametr bwaitall ma wartość FALSE, to zwrócona wartość pomniejszona o stałą WAIT _ ABANDONED _ 0 określa, który z obiektów wskazywanych w tablicy lphandles przeszedł w stan zasygnalizowany i wcześniej stając się właścicielem muteksu nie zwolnił go, WAIT _ TIMEOUT oczekiwanie zakończyło się po upływie określonego okresu czasu, WAIT _ FAILED błąd podczas wykonania funkcji; rodzaj błędu zwraca funkcja GetLastError. Funkcje oczekujące mają jedną nieusuwalną wadę. W najczęściej używanym trybie z oczekiwaniem nieskończonym zużywają czas procesora. Alternatywną możliwości jest stosowanie oczekiwania biernego. Można do wykorzystać funkcję Sleep usypiającą wątek na określony czas. Ale funkcja ta może być użyta tylko wewnątrz funkcji wątku. innym rozwiązaniem jest zawieszenie wykonywania wątku przy użyciu funkcji SuspendThread. Funkcja ta może być wywołana także spoza wnętrza funkcji wątku. Sekcje krytyczne Niskopoziomowym mechanizmem API WIN32 umożliwiającym synchronizację wątków są sekcje krytyczne. Sekcja krytyczna pozwala na objęcie nieprzerywalnością wykonania wybranego fragmentu funkcji wątku celem np. ograniczenia dostępu do jakiegoś zasobu lub pliku. Podobnie jak operacje atomowe sekcje krytyczne ograniczone są zasięgiem swojego działania do granic jednego procesu. Natomiast nie ma ograniczeń (poza oczywiście wielkością dostępnej pamięci) do ilości sekcji krytycznych w programie. Od strony technicznej sekcja krytyczna w API WIN32 to struktura o nazwie CRITICAL _ SECTION, do której dostęp powinien odbywać się wyłącznie za pomocą funkcji API. Pierwszą czynnością niezbędną przed wykorzystaniem sekcji krytycznej jest jej inicjalizacja. Umożliwiają to funkcje InitializeCriticalSection i InitializeCriticalSectionAndSpinCount. Pierwsza z nich wymaga tylko jednego parametru wskaźnika do struktury CRITI- CAL _ SECTION. Druga pozwala dodatkowo na ustalenie początkowej wartości wewnętrznego licznika czasu trwania pętli aktywnego czekania (parametr ten jest ignorowany na komputerach jednoprocesorowych). Wartość licznika pętli aktywnego oczekiwania można zmodyfikować także później korzystając z funkcji Set- CriticalSectionSpinCount. Fragment funkcji wątku objęty sekcją krytyczną rozpoczyna wywołanie funkcji EnterCriticalSection, a kończy funkcja LeaveCriticalSection. Funkcja EnterCriticalSection wykonuje pętlę aktywnego czekania tak długo, aż dana sekcja krytyczna zostanie zwolniona. Wiąże się to oczywiście z użyciem czasu pracy procesora. Alternatywnym rozwiązaniem jest sprawdzenie przy pomocy funkcji TryEnterCriticalSection, czy danej sekcji krytycznej nie używa aktualnie inny wątek i podjęcie decyzji np. o wykonywaniu innych działań przez wątek bądź przejście w stan oczekiwania biernego. Po zakończeniu korzystania z sekcji krytycznej należy zwolnić przydzielone jej zasoby wywołując funkcję DeleteCriticalSection. Muteksy Muteks jest obiektem, który posiada dwa stany: sygnalizowany i niesygnalizowany. W danej chwili właścicielem muteksu może być tylko jeden obiekt. W praktyce od sekcji krytycznej muteksy odróżnia możliwość działania poza obszarem jednego procesu. Muteks tworzy funkcja CreateMutex, która posiada następujące parametry: lpmutexattributes wskaźnik na strukturę SECURITY _ ATTRI- BUTES określającej atrybuty zabezpieczeń i dostępu do mu- 36 www.sdjournal.org Software Developer s Journal 4/2006
Programowanie wielowątkowe w WIN32 teksu; w większości przypadków wystarczy przyjęcie ustawień domyślnych i podanie wartości NULL, binitialowner wartość TRUE powoduje, że właścicielem utworzonego muteksu jest wątek, w którym go utworzono, lpname nazwa muteksu, podanie wartości NULL spowoduje utworzenie muteksu nienazwanego. Funkcja CreateMutex zwraca uchwyt do utworzonego muteksu. Jeżeli jego utworzenie nie powiodło się funkcja zwraca wartość NULL. Po zakończeniu używania muteksu należy zwolnić przedzielone mu zasoby wywołując funkcję CloseHandle. Aby stać się właścicielem muteksu obiekt musi wywołać funkcję oczekującą. Jeżeli muteks będzie w stanie sygnalizowanym, to wątek wołający funkcję oczekującą zostaje jego właścicielem, aż do wywołania funkcji ReleaseMutex, która powoduje zwolnienie muteksu i jego przejście w stan niesygnalizowany. System gwarantuje, że w danym momencie muteks może mieć tylko jednego właściciela. Zakres działania muteksów nie jest ograniczony do jednego procesu, stąd każdy wątek korzystając z funkcji OpenMutex może uzyskać uchwyt do muteksu nazwanego. Zakres dostępu do muteksu reguluje parametr dwdesiredaccess, którego standardowa wartość wynosi MUTEX _ MODIFY _ STATE, a wartość MU- TEX_ALL_ACCESS oznacza pełny dostęp do muteksu, przy czym aplikacja musi wówczas działać z prawami administratora. Semafory Semafory dostępne w API WIN32 przechowują wartości całkowite nieujemne z wybranego zakresu. Wartość początkowa semafora określa ilość wątków, które mogą być jednocześnie obsługiwane, czyli np. mają dostęp do wybranego zasobu. Każdy wątek wchodzący do obszaru chronionego przez semafor zmniejsza wartość jego licznika o 1. Jeżeli licznik semafora osiągnie wartość 0, następne wątki nie zostaną dopuszczone do chronionego obszaru, aż do czasu, gdy któryś z wątków nie wyjdzie z obszaru chronionego. Utworzenie semafora umożliwia funkcja CreateSemaphore, która posiada następujące parametry: lpsemaphoreattributes wskaźnik na strukturę SECURITY _ AT- TRIBUTES określającej atrybuty zabezpieczeń i dostępu do semafora; w większości przypadków wystarczy przyjęcie ustawień domyślnych i podanie wartości NULL, linitialcount początkowa wartość licznika semafora, lmaximumcount maksymalna wielkość licznika semafora (liczba całkowita większa od 0), lpname nazwa semafora, podanie wartości NULL spowoduje utworzenie semafora nienazwanego. Funkcja CreateSemaphore zwraca uchwyt do utworzonego semafora. Jeżeli utworzenie semafora nie powiodło się funkcja zwraca wartość NULL. Po zaprzestaniu korzystania z semafora należy zwolnić przedzielone mu zasoby wywołując funkcję CloseHandle. Wejście do sekcji chronionej przez semafor umożliwiają funkcje oczekujące. Semafor jest w stanie sygnalizowanym, gdy wartość jego licznika jest większa od 0. Natomiast zwiększenie wartości licznika semafora umożliwia funkcja ReleaseSemaphore, która zwraca jednocześnie poprzednią wartość licznika. Zakres dostępu do semafora reguluje parametr dwdesiredaccess, którego standardowa wartość wynosi SEMAPHORE _ MODI- FY _ STATE (możliwość zwiększenia licznika semafora przy użyciu funkcji ReleaseSemaphore), a wartość SEMAPHORE _ ALL _ ACCESS oznacza pełny dostęp do semafora, przy czym aplikacja musi wówczas działać z prawami administratora. Zdarzenia Zdarzenia (ang. event) są obiektami dwustanowymi: zdarzenie jest w stanie sygnalizowanym (zgłoszonym) albo niesygnalizowanym (niezgłoszonym). Od muteksów zdarzenia odróżnia możliwość regulacji sposobu przejścia zdarzenia w stan niesygnalizowany. Zgłoszenie tworzy funkcja CreateEvent, która posiada następujące argumenty: lpeventattributes wskaźnik na strukturę SECURITY _ ATTRI- BUTES określającej atrybuty zabezpieczeń i dostępu do semafora; w większości przypadków wystarczy przyjęcie ustawień domyślnych i podanie wartości NULL, bmanualreset określenie sposobu przechodzenia zdarzenia w stan niesygnalizowany; wartość TRUE oznacza, że będzie wymagane wywołanie funkcji ResetEvent; wartość FALSE oznacza, że przejście zdarzenia w stan niesygnalizowany nastąpi automatycznie po zakończeniu działania funkcji oczekującej, binitialstate określenie czy po utworzeniu zdarzenie ma być w stanie sygnalizowanym (TRUE) czy też niesygnalizowanym (FALSE), lpname nazwa zdarzenia, podanie wartości NULL spowoduje utworzenie zdarzenia nienazwanego. Funkcja CreateEvent zwraca uchwyt do utworzonego zdarzenia. Jeżeli utworzenie zdarzenia nie powiodło się funkcja zwraca wartość NULL. Po zaprzestaniu korzystania ze zdarzenia należy zwolnić przedzielone mu zasoby wywołując funkcję CloseHandle. Wybór ręcznego sposobu przejścia zdarzenia w stan niesygnalizowany umożliwia np. zdalne i jednoczesne wywołanie wielu wątków znajdujących się w fazie oczekiwania na wejście do obszaru chronionego. Zdarzeń można także użyć do zdalnego usunięcia jednego lub grupy wątków. Niezależnie od sposobu przechodzenia zdarzenia w stan niesygnalizowany jego powrót do stanu sygnalizowanego wymaga wywołania funkcji SetEvent. Warto również omówić funkcję PulseEvent, która powoduje przejście zdarzenia w stan sygnalizowany i natychmiastowy jego powrót w stan niesygnalizowany. Reakcja wątków oczekujących na zdarzenie zależy od wybranego sposobu przejścia zdarzenia w stan niesygnalizowany. Jeżeli jest to wykonywane ręcznie wywołanie PulseEvent przepuszcza wszystkie oczekujące wątki. W przeciwnym wypadku z puli wątków oczekujących na zasygnalizowanie zdarzenia wybierany jest tylko jeden. Trzeba jednak pamiętać, że specyfikacja API WIN32 odradza korzystnie z tej funkcji. Jest ona utrzymywana wyłącznie dla zachowania kompatybilności z istniejącymi programami i nie powinna być używana w nowo tworzonym oprogramowaniu. Podobnie jak w przypadku semaforów i muteksów zakres działania zdarzeń nie ogranicza się do jednego procesu. Dostęp do zdarzenia nazwanego można uzyskać za pomocą funkcji OpenEvent. Zakres dostępu określa parametr dwdesiredaccess, którego wartość EVENT _ MODIFY _ STATE oznacza możliwość modyfikacji stanu zdarzenia, a wartość EVENT _ ALL _ ACCESS pełny dostęp do zdarzenia, przy czym aplikacja musi wówczas działać z prawami administratora. Software Developer s Journal 4/2006 www.sdjournal.org 37
Programowanie C/C++ Kolejny przykładowy program przedstawia użycie: sekcji krytycznej, muteksu, semafora i zdarzenia do tego samego zadania synchronizacji zapisu danych do pliku. Jednocześnie porównywany jest czas działania każdego z mechanizmów synchronizacji wątków. Wynik są nieco zaskakujące, bo najszybsze okazały się zdarzenia, nieco dłużej trwała synchronizacja przy użyciu sekcji krytycznych. Zegary oczekujące Ostatnim opisywanym mechanizmem synchronizacji wątków są zegary oczekujące (ang. waitable timer). Zegar oczekujący przechodzi w stan sygnalizowany po upływie zadanego okresu czasu lub w określonych odstępach czasu z automatycznym powrotem do stanu niesygnalizowanego. Zegary umożliwiają np. regularne wywoływanie określonych wątków. Zegar oczekujący tworzy funkcja CreateWaitableTimer, która posiada następujące argumenty: lptimerattributes wskaźnik na strukturę SECURITY _ ATTRI- BUTES określającej atrybuty zabezpieczeń i dostępu do zegara; w większości przypadków wystarczy przyjęcie ustawień domyślnych i podanie wartości NULL, bmanualreset wartość TRUE oznacza, że zegar jest ustawiany ręcznie; wartość FALSE oznacza, że zegar jest przestawiany w stan sygnalizowany automatycznie w określonych odstępach czasu z automatycznym powrotem do stanu niesygnalizowanego po wyjściu z funkcji oczekującej, lptimername nazwa zegara, podanie wartości NULL spowoduje utworzenie zegara nienazwanego. Funkcja CreateWaitableTimer zwraca uchwyt do utworzonego zegara oczekującego. Jeżeli jego utworzenie nie powiodło się funkcja zwraca wartość NULL. Po zakończeniu używania zegara należy zwolnić przedzielone mu zasoby wywołując funkcję Close- Handle. Po utworzeniu zegar oczekujący znajduje się w stanie nieaktywnym i niesygnalizowanym. Celem aktywacji zegara należy wywołać funkcję SetWaitableTimer, Z jej sześciu parametrów omówimy trzy początkowe: htimer uchwyt zegara, pduetime czas, po którym zegar przejdzie w stan sygnalizowany; czas jest opisywany 64-bitową liczbą 64 (LARGE _ INTEGER) z dokładnością do 100 nanosekund; wartość ujemna oznacza czas względny, wartość dodatnia czas absolutny UTC (ang. Coordinated Universal Time), lperiod okres czasu (w milisekundach) po upływie którego zegar przechodzi w stan sygnalizowany; jeżeli podana wartość jest większa od zera, to zegar jest automatycznie uruchamiany co określony czas dopóki nie zostanie zatrzymany poprzez wywołanie funkcji CancelWaitableTimer lub ponownie aktywowany przy użyciu funkcji SetWaitableTimer. Parametry działania zegara możemy w każdej chwili zmienić wywołując funkcję CreateWaitableTimer. W razie potrzeby zegar można przestawić w tryb nieaktywny wywołując funkcję Cancel- WaitableTimer. Podobnie jak wcześniej opisane obiekty wspierane przez jądro WIN32, także zegary oczekujące mogą działać poza granicami jednego procesu. Dostęp do zegara nazwanego wymaga wywołania funkcji OpenWaitableTimer. Zakres dostępu określa parametr dwdesiredaccess, którego wartość TIMER _ MODIFY _ STATE oznacza możliwość modyfi kacji stanu zegara, a wartość TIMER _ ALL _ ACCESS pełny dostęp do zegara, przy czym aplikacja musi wówczas działać z prawami administratora. Program tworzy dwa zegary oczekujące, które są wykorzystywane przez dwa oddzielne wątki. Pierwszy zegar działa w trybie automatyczny, a wątek z niego korzystający co 2 sekundy manifestuje swoją obecność odpowiednim napisem. Drugi zegar działa w trybie ręcznym, stąd wątek z niego korzystający musi za każdym razem ponownie ustawiać zegar wywołując funkcję SetWaitableTimer. Zarządzanie wątkami Do funkcji pozwalających zarządzający wątkami można zaliczyć: Sleep uśpienie wątku na określony czas, SuspendThread zawieszenie wykonywania wątku, ResumeThread wznowienie wykonywania wątku (po wcześniejszym zawieszeniu), TerminateThread przerwanie działania wątku, SwitchToThread przekazanie kwantu czasu procesora innemu wątkowi. Z wyjątkiem Sleep powyższe funkcje wymagają posiadania uchwytu wątku. Jeżeli uchwyt jest z jakichkolwiek powodów niedostępny można go uzyskać, przy użyciu opisanej wcześniej funkcji GetCurrentThread. Jeżeli natomiast mamy identyfi kator wątku, uchwyt można uzyskać przy pomocy funkcji OpenThread. API WIN32 dostarcza także możliwości ustawienia priorytetu wykonania wątku względem innych wątków procesu. Oznacza, to że na ostateczny priorytet wątku ma wpływ także priorytet procesu, w ramach którego działa wątek. Zmianę priorytetu wątku realizuje funkcja SetThreadPrioryty, której drugi parametr określa wartość priorytetu wątku. Normalny i zarazem domyślny priorytet wątku defi niuje stała THRE- AD _ PRIORITY _ NORMAL, priorytety obniżone to: THREAD _ PRIO- RITY_BELOW_NORMAL, THREAD _ PRIORITY _ LOWEST i THREAD _ PRIORITY _ IDLE, natomiast priorytety podwyższone: THREAD _ PRIORITY _ ABOVE _ NORMAL, THREAD _ PRIORITY _ HIGHEST i THRE- AD _ PRIORITY _ TIME _ CRITICAL. Odczyt bieżącego priorytetu wątku umożliwia funkcja GetThreadPriority. Planista zaimplementowany w systemach Windows stara się w miarę potrzeby dynamicznie zwiększać priorytet wątków. Pewną kontrolę tego mechanizmu umożliwiają funkcje SetThreadPriorityBoost i GetThreadPriorityBoost. Generalnie decyzję o zmianie priorytetów wątków należy podejmować z ostrożnością. Przyjęcie wysokiego priorytetu dla wątku wykonującego pracochłonne zadanie może spowodować wyraźne obniżenie wydajności całego systemu. Zdecydowanie lepiej do podwyższonego priorytetu nadają się wątki wykonujące swoje zadania krótko i rzadko zwłaszcza, gdy znaczenie ma szybkość reakcji na określone zdarzenie. W takich przypadkach odpowiednie podwyższenie priorytetu wątku może podnieść komfort użytkowania programu. 38 www.sdjournal.org Software Developer s Journal 4/2006
Programowanie wielowątkowe w WIN32 Wątek jest uruchamiany trzykrotnie: z normalnym, najwyższym i najniższym priorytetem. Przy nieobciążonym systemie różnice w czasach działania nie są duże; dopiero większa ilość równocześnie wykonywanych zadań pokazuje istotne różnice. Przykładowy efekt działania programu w warunkach dużego obciążenia systemu (kilka programów kompresujących działających w tle). W komputerach wieloprocesorowych można dodatkowo ustalać działanie wątków na poszczególnych procesorach: SetThreadAffinityMask wskazanie grupy procesorów, na których może działać wątek; procesory wskazuje bitowa maska powinowactwa, w której najmłodszy bit oznacza pierwszy procesor, a najstarszy bit ostatni procesor, SetThreadIdealProcessor wskazanie najlepszego procesora do wykonania wątku (jest to zalecenie dla planisty, które nie musi zostać zrealizowane); procesory numerowane są od 0; podanie wartości MAXIMUM _ PROCESSORS oznacza, że dany watek nie ma preferowanego procesora. Maska powinowactwa podawana w funkcji SetThreadAffinityMask musi być podzbiorem analogicznej maski powinowactwa określonej dla procesu (jest to tzw. mechanizm dziedziczenia). Odczyt maski powinowactwa procesu umożliwia funkcja GetProcessAffinityMask, a do jej zmiany należy użyć funkcji SetProcessAffinityMask. Funkcja GetProcessAffinityMask zwraca także maskę powinowactwa procesorów dostępnych w systemie. Jest to ważna informacja, ponieważ nie zawsze możemy zakładać dostępność wszystkich procesorów dla danego procesu, a w konsekwencji także i wątków tego procesu. Ustalenie ilości procesorów dostępnych w systemie umożliwia funkcja GetSystemInfo, której jedyny parametr stanowi wskaźnik do struktury SYSTEM _ INFO. Wśród informacji zawartych w tej strukturze interesują nas dwa pola: dwnumberofprocessors ilość procesorów dostępnych w systemie, dwactiveprocessormask maska powinowactwa określająca aktywne procesory w systemie. Kompilowanie programów wielowątkowych W przypadku korzystania z kompilatora Borland C++ Compiler 5.5 w linii poleceń (albo w pliku make) trzeba dodać parametr -twm lub -WM. Użytkownicy kompilatora MinGW powinni natomiast dodać w linii poleceń parametr -mthreads. Jeżeli do pracy z kompilatorem MinGW używamy Code::Blocks IDE, opcję tę dopisujemy w okienku Project's Build options w zakładce Compiler/ Other options. W kompilatorze Microsoft Visual C++2005 Express Edition opcję kompilacji programu wielowątkowego wybieramy w okienku Property Pages. Jest to odpowiednik parametru /MT w linii poleceń (/MTd dla programu przygotowanego do debugowania). Zwykle najlepiej jest pozostawić zadania związane z szeregowaniem wątków systemowi operacyjnemu. Jednak wsparcie systemu może w specyfi cznych sytuacjach poprawić wydajność programu. Trzeba przy tym pamiętać, że na rynku komputerów PC dostępne są komputery o dwóch różnymi architekturach wieloprocesorowych. Poza klasyczną architekturą SMP (ang. Symmetric Multiprocessing) produkowane są także komputery z architekturą NUMA (ang. Non-Uniform Memory Access), gdzie ograniczenie działania wątków procesu do granic jednego węzła procesorów może zwiększyć szybkość działania całego procesu. Samodzielną optymalizację programów utrudnia dodatkowo stosowana przez Intela technologia HT (ang. HyperThreading), stosowana zarówno w procesorach jedno jak i dwurdzeniowych, bowiem hiperwątkowość oferowana przez pojedynczy procesor oferuje mniejszą wydajność od fi zycznego zwielokrotnienia CPU. Jeden z pięciu tworzonych wątków jest kierowany na pierwszy procesor, cztery pozostałe wątki na drugi procesor. Po wymuszonej przerwie te same wątki wykonywane są ponownie ale już bez żadnych wskazówek dla systemowego planisty. Testy aplikacji GUI Dotychczasowe programy przykładowe działały w trybie konsolowym. Testy wielowątkowości w systemie Windows nie byłyby jednak kompletne bez testów programu działającym w środowisku grafi cznym. Przykładowy program udostępnia dwa testy. Pierwszy test tworzy 4 wątki wykonujące niezależne od siebie obliczenia, drugi wykonuje te same obliczenia wywołując kolejno cztery razy tę samą funkcję. Czas wykonania obu testów wyświetlany jest w oknie. Wątki wykonały swoje zadanie ponad siedmiokrotnie szybciej od funkcji.efekt działania tego samego programu uruchomionego na komputerze z procesorem Celeron II (jądro Coppermine-128) pracującym z częstotliwością 533 MHz. Także w tym przypadku wątki okazały się szybsze, choć tylko trzykrotnie. Oba programy uruchomione były w systemie Windows XP. Różnica w czasie wykonania testu przez wątki nie powinna dziwić biorąc pod uwagę, że system Windows szereguje zadania na poziomie wątków. Pięć wątków (1 wątek główny procesu i 4 wątki utworzone w trakcie pracy) otrzymało łącznie większą ilość czasu czasu procesora niż jeden wątek. Poza łatwymi do policzenia wskaźnikami, jak czas pracy, wątki dają aplikacji GUI jeszcze inną zaletę. Podczas ich wykonywania nie jest blokowane główne okno programu, które jest typowym efektem dużego obciążenia pracą. Oczywiście efekt ten można uzyskać także innymi metodami, ale nie jest to tak eleganckie jak w przypadku zastosowania wątków. Podsumowanie Niniejszy artykuł stanowi zaledwie wstęp do bardzo obszernych zagadnień związanych z programowaniem wielowątkowym w systemach WIN32. I to nie ukrywajmy wstęp niepełny, bowiem nieopisane zostały jeszcze takie mechanizmy jak pamięć lokalna wątku TLS (ang. Thread Local Storage), włókna (ang. fi b e r ) pule wątków. Mam jednak nadzieję, że jego lektura skłoni osoby nieznające prezentowanych technik do zapoznania się z nimi i stosowania w swoich programach. A tematykę tę warto poznać, bo przyszłość przedstawiana przez producentów procesorów jaki się rosnącą ilością rdzeni w procesorach... Software Developer s Journal 4/2006 www.sdjournal.org 39