Transcript Wątki i ich synchronizacja
Wątki i ich synchronizacja
Jakub Szatkowski Programowanie w środowisku Windows 03.12.2010
Plan prezentacji
Pojęcia: proces, wątek Po co nam wątki? Tworzenie wątków.
Kontekst wątku, priorytety wątków Problemy dotyczące synchronizacji wątków Metody synchronizacji wątków Krótkie omówienie bibliotek
Proces
Procesem można nazwać wykonujący się program. Do wykonania swojej pracy proces potrzebuje na ogół pewnych zasobów takich jak czas procesora, pamięć operacyjna czy urządzenie wejścia-wyjścia. Zasoby te są przydzielane powstania działania.
procesowi lub podczas w chwili jego późniejszego
Proces i wątek
W celu przydziela konieczne wskazane wykonania programu procesowi zasoby współbieżne fragmentów programu działających na tych samych danych. Aby to utworzenia zrealizować program może zażądać określonej liczby wątków wykonujących części programu. Wątki te współdzielą wszystkie zasoby procesu jest przydzielany ale system wykonywanie każdemu wątkowi osobno.
operacyjny także może być pewnych oprócz czasu procesora, który Przykład:
WczytajDane(&a1); PrzetwórzDane(&a1); WypiszDane(a1); WczytajDane(&a1); PrzetwórzDane(&a1); i WczytajDane(&a2); WypiszDane(&a1); i PrzetworzDane(&a2);
Wątek
Wątek, jest to jednostka wykonawcza w obrębie jednego procesu, będąca kolejnym ciągiem instrukcji wykonywanym w Obiekt obrębie tych samych danych (w tej samej przestrzeni adresowej).
Wątek składa się z dwóch elementów: obiektu jądra oraz stosu wątku.
wątek jest używany przez system operacyjny do zarządzania wątkiem.
stosie wykonania kodu "pojemnikiem" przynajmniej jeden Ponadto, przechowuje w tym obiekcie informacje o system wątku. Na wątku natomiast, trzymane są wszystkie parametry funkcji i zmienne lokalne potrzebne do wątku. Należy wiedzieć, że sam proces w zasadzie niczego nie wykonuje. Jest tylko zawierającym wątki. Proces musi mieć wątek, tzw. wątek główny, ale możliwe jest utworzenie większej liczby wątków w kontekście jednego procesu. Co za chwilę uczynimy.
Wątek
Jednostka dla której system przydziela czas procesora Każdy proces ma co najmniej jeden wątek Proces nie wykonuje kodu, proces jest obiektem dostarczającym wątkowi przestrzeni adresowej i odpowiednich zasobów Kod zawarty w procesie jest wykonywany przez wątek Pierwszy wątek procesu tworzony jest automatycznie przez system operacyjny, każdy następny musi być utworzony przez programistę Wszystkie wątki tego samego procesu dzielą przestrzeń adresową i mają dostęp do tych samych zmiennych globalnych i zasobów systemowych.
Po co nam wątki?
Wiemy, że wieloprogramowość sprawia, że efektywniej wykorzystujemy procesor. Tak samo wątki w danym procesie sprawiają, że ten proces wykorzystuje efektywniej przydzielony mu czas procesora.
Jako przykład posłużmy się żmudnym rekurencyjnym wyliczaniem 3 wartości wyrazów ciągu Fibonacciego i 2 sortowań bąbelkowych tablic liczb całkowitych, wszystkie wyniki zapisywane są do plików i porównamy czas działania programu gdzie mamy 5 wątków i każdy z nich liczy odpowiedni wyraz ciągu lub tworzy i sortuje tablicę, a następnie zapisuje do pliku swój wynik i programu gdzie 1 wątek po kolei wykonuje te same instrukcje.
Funkcje tworząca wątek
Aby stworzyć wątek w windows używamy funkcji:
HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, DWORD cbStack, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId );
Argumenty funkcji CreateThread
PSECURITY_ATTRIBUTES psa
Wskaźnik do struktury SECRUITY_ATTRIBUTES, która wygląda tak:
typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES
Każda funkcja tworząca dowolny obiekt jądra pobiera wskaźnik do tej struktury. Parametr
nLength
określa rozmiar struktury, parametr
lpSecurityDescriptor
zainicjalizowanego deskryptora to adres bezpieczeństwa. Parametr
bInheritHandle
decyduje on o tym, czy tworzony obiekt jądra jest dziedziczny. My nie będziemy dziedziczyć wątków i będziemy chcieli ustawić standardowe atrybuty bezpieczeństwa, dlatego będziemy przekazywać NULL.
Argumenty funkcji CreateThread
DWORD cbStack
Określa ilość przestrzeni adresowej jaką wątek może przeznaczyć na swój stos (każdy wątek go posiada). Podanie wartości 0 powoduje przydzielenie stosowi wątku całej pamięć zarezerwowana dla tego wątku.
Dla przypomnienia: DWORD ≈ Unsigned Int
Argumenty funkcji CreateThread
PTHREAD_START_ROUTINE pfnStartAddr
Określa adres funkcji od której ma się zacząć wykonywanie wątku. Przykładowa definicja takiej funkcji wygląda tak:
DWORD WINAPI FunkcjaWatku(PVOID pvParam){ cout<<"Przekazana wartosc:” <<*(int*)pvParam; return 0; } PVOID pvParam
Dowolny parametr przekazywany do funkcji wątkowej.
Argumenty funkcji CreateThread
DWORD fdwCreate
Określa dodatkowe flagi. Podanie 0 powoduje, że wątek jest od razu wprowadzony do kolejki wykonania, podanie w tym miejscu flagi: CREATE_SUSPENDED spowoduje, że wątek po utworzeniu będzie zawieszony i będzie oczekiwał na wznowienie go funkcją ResumeThread( HANDLE hThread ) gdzie jako argument podajemy uchwyt do wątku. W każdej chwili możemy wątek zawiesić funkcją SuspendThread(HANDLE hThread);
Argumenty funkcji CreateThread
PDWORD pdwThreadId
Ostatni parametr funkcji
CreateThread
musi przekazywać adres zmiennej typu DWORD, która będzie przechowywać identyfikator (ID) utworzonego wątku.
Np. deklarujemy DWORD watek1; i wywołujemy CreateThread(…,&watek1);
Zakończenie wątku
Istnieją różne sposoby zakończenia wątku w Windows: VOID ExitThread( DWORD dwExitCode ); ta funkcja zabija wątek, który ją wywołał, jej parametr określa kod wyjścia wątku. BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode ); Pozwala zabić dowolny wątek do którego podamy uchwyt.
Wątek zginie też wtedy gdy zabijemy proces, w przestrzeni którego owy wątek działał.
Gdy funkcja wątkowa zakończy swoje działanie to nie musimy wywoływać żadnej funkcji zamykania, watek zakończy się samoistnie.
Tworzenie wątku, co się dzieje?
Kiedy wywołujemy funkcję CreateThread() z odpowiednimi parametrami tworzymy nowy obiekt jądra i system inicjalizuje pewne jego własności takie jak: licznik użyć, licznik zawieszeń, kod wyjścia i stan obiektu. System alokuje pamięć na stos wątku, która pochodzi z przestrzeni adresowej procesu do którego należy ów wątek. Do stosu zapisywany jest adres funkcji przekazywanej w CreateThread(), a także jej parametr. Każdy wątek ma też swój zbiór rejestrów CPU zwany kontekstem. Kontekst przedstawia stan obiektu jądra jakim jest wątek. Zbiór rejestrów przechowywany jest w strukturze CONTEXT. Przechowywane w niej są m.in. rejestr wskaźnika instrukcji i rejestr wskaźnika stosu.
Kontekst wątku
To zbiór rejestrów CPU z których wątek korzysta. Kiedy mija czas wykonywania wątku system przerywa jego działania i bierze z kolejki kolejny wątek. Aby pierwszy wątek mógł dalej kontynuować swoje działanie rejestry CPU muszą mieć taką samą wartość jak w momencie przerwania działania. Te wartości są przechowywane właśnie w kontekście wątku. Następuje przełączanie kontekstów wraz z obsługą kolejnych wątków. Struktura CONTEXT pozwala systemowi pamiętać stan wątku i podjąć działanie od tego momentu, w którym ostatnio wątek je zakończył. Programista jest w stanie w dowolnej chwili odczytać kontekst wątku funkcją GetThreadContext(HANDLE hThread, PCONTEXT pContext);
Kontekst wątku
W przykładowej aplikacji wyciągamy kontekst wątku i sprawdzamy rejestry CPU Eax, Ebx, Ecx i Edx są to rejestry ogólnego przeznaczenia. Są to odpowiednio:
EAX EBX
– rejestr akumulacji – rejestr bazowy
ECX EDX
– rejestr licznika – rejestr danych Windows daje nam możliwość dowolnego ustawienia rejestrów w kontekście dzięki funkcji BOOL SetThreadContext(HADLE hThread, CONST CONTEXT *pContext);
Zatrzymanie wątku
Wątek możemy uśpić używając do tego funkcji:
VOID Sleep( DWORD dwMilliseconds);
Wątek w którym wywołamy tę funkcję zostanie zawieszony na czas określony przez parametr oddając przy tym swój czas korzystania z CPU.
Natomiast funkcja:
BOOL SwitchToThread();
Sprawia, że jeśli istnieje inny wątek oczekujący jako pierwszy w kolejce na przydział CPU to system przydzieli mu natychmiastowo kwant czasu na wykonanie. Przydatna np. gdy chcemy przydzielić procesor wątkowi o niższym priorytecie
Priorytety wątku
Wątki mogą mieć różne priorytety co ma decydujący wpływ na to, w której kolejności się wykonają.
Każdy wątek ma przypisany bazowy poziom priorytetu będący liczbą z przedziału 0-31 (31 –najwyższy priorytet, 0-najniższy).
Algorytm przydzielający CPU wątkom działa tak by najpierw obsłużyć wątki o wyższym priorytecie. System Windows jest systemem z wywłaszczeniem, a więc jeśli procesor jest przydzielony wątkowi o małym priorytecie, a w kolejce pojawi się wątek o większym priorytecie natychmiastowo zatrzymuje działanie wątku o niższym priorytecie i daje możliwość wykonania się wątku o wyższym priorytecie.
Wątki mogą zmieniać swoje priorytety
Priorytety wątku
Priorytet jest określany na podstawie dwóch wartości: klasy priorytetu procesu w którym wykonuje się wątek i względnego priorytetu wątku w obrębie procesu.
W systemie Windows istnieje 6 klas priorytetów (dotyczą procesów!) o następujących właściwościach: Czasu rzeczywistego składowe systemu) (wątki wywłaszczają nawet Wysoki – np. Eksplorator Windows lub Menadżer Zadań Powyżej normalnego Normalny – standardowa klasa w systemie Windows, jeśli tworzymy proces nie określając klasy priorytetu to właśnie ta klasa jest mu przydzielana Poniżej normalnego Niski – wątki wykonują się w momentach bezczynności systemu, np. wygaszacz ekranu.
Priorytety wątków
Kolejną rzeczą określającą priorytet wątku jest względny priorytet wątku. Windows ma 7 takich rodzajów priorytetów: Krytyczny Najlepszy Powyżej normalnego Normalny Poniżej normalnego Najgorszy Niski Jeżeli chce się ustalić priorytet wątku jako liczbę od 0 do 31 to należy ustalić zarówno klasę priorytetu procesu jak i względny priorytet wątku.
Priorytety wątków
Względny priorytet Krytyczny Najlepszy Powyżej normalnego Normalny Poniżej normalnego Najgorszy Niski
Niski
15 6 5 Klasa priorytetu procesu
Poniżej normalnego
15 8 7
Normalny
15 10 9
Powyżej normalnego
15 12 11
Wysoki
15 15 14
Czasu rzeczywistego
31 26 25 4 3 2 1 6 5 4 1 8 7 6 1 10 9 8 1 13 12 11 1 24 23 22 16
Klasa priorytetu procesu
Aby ustawić klasę priorytetu procesu można wywołać proces funkcją CreateProcess() i parametrze fwdCreate ustawić którąś z poniższych flag:
• REALTIME_PRIORITY_CLASS - czasu rzeczywistego.
• • • • • HIGH_PRIORITY_CLASS - wysoki.
ABOVE_NORMAL_PRIORITY_CLASS IDLE_PRIORITY_CLASS - niski. powyżej normalnego.
NORMAL_PRIORITY_CLASS - normalny.
BELOW_NORMAL_PRIORITY_CLASS poniżej normalnego.
Można również użyć funkcji:
BOOL SetPriorityClass(HANDLE hProcess,DWORD fdwPriority);
Jeśli chcemy sprawdzić jaką nasz proces ma klasę priorytetu możemy użyć funkcji:
DWORD GetPriorityClass( HANDLE hProcess);
Funkcja zwraca jeden z identyfikatorów podanych powyżej.
Priorytet względny wątku
Priorytet względny ustawiamy wywołując funkcję:
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
W parametrze nPriority przekazujemy jeden z poniższych identyfikatorów określających priorytet:
• • • • • • • THREAD_PRIORITY_TIME_CRITICAL - krytyczny.
THREAD_PRIORITY_HIGHEST - najlepszy.
THREAD_PRIORITY_ABOVE_NORMAL powyżej normalnego.
THREAD_PRIORITY_NORMAL - normalny.
THREAD_PRIORITY_BELOW_NORMAL poniżej normalnego.
THREAD_PRIORITY_LOWEST - najgorszy.
THREAD_PRIORITY_IDLE - niski.
Gdy chcemy pobrać względny priorytet wątku należy użyć funkcji
GetThreadPriority(handle hThread);
i przekazać jej odpowiedni uchwyt do wątku.
Podwyższanie priorytetów
Zdarza się, że system sam podwyższa priorytet danego wątku. Gdy mamy sytuację gdy wątek o małym priorytecie nie może uzyskać dostępu do CPU bo wykonują się wątki o wyższych priorytetach i system taką sytuację wykryje to podwyższa priorytet tego wątku do 15 i pozwala mu się wykonać przez dwa kwanty czasu poczym przywraca mu priorytet do wartości bazowej.
Jeśli chcemy możemy w swoim programie zablokować takie działania systemu używając funkcji:
BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL DisablePriorityBoost);
lub wyłączyć taki mechanizm dla danego wątku:
BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL DisablePriorityBoost);
Sprawdzenie włączenia to oczywiście:
BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL pDisablePriorityBoost);
lub:
BOOL GetThreadPriorityBoost(HANDLE hThread, PBOOL pDisablePriorityBoost);
Wątki i priorytety - praktyka
Ustawimy priorytet procesu jakim jest nasz program na powyżej normalnego Utworzymy dwa wątki, jednego względy priorytet ustawimy na poniżej normalnego a drugi na najlepszy Pobierzemy od systemu uchwyt do głównego wątku naszego programu i ustawimy jego względny priorytet na najniższy Każdy wątek będzie 50 razy wyświetlał informacje o swoim numerze i liczniku wyświetleń
Dlaczego synchronizacja?
Jak widzieliśmy w poprzednim programie gdy na chwilę zaczął działać jakiś wątek to inny wątek przerywał jego tekst i pisał swój. By tak nie było potrzebny będzie jakiś mechanizm synchronizacji używany najczęściej przy dostępie wątków do współdzielonych danych lub urządzeń.
Gdyby dwa różne wątki coś drukowały na drukarce i nie byłyby one zsynchronizowane nie otrzymalibyśmy satysfakcjonującego nas wydruku.
Dlaczego synchronizacja?
Wątki współzawodniczą o dostęp do zasobów. Zasoby te najczęściej w danej chwili mogą być wykorzystywane tylko przez jeden wątek (lub przez liczbę wątków mniejszą od chętnych). To dość często spotykana sytuacja w życiu codziennym. Np. poranne korzystanie z łazienki. Przecież najpierw czekamy aż łazienka się zwolni, a potem z niej korzystamy. Gdy jednak dwie osoby naraz chcą wejść do łazienki to albo porównujemy priorytety swoich potrzeb albo ktoś musi zastosować zasadę uprzejmości.
Zasób dzielony i sekcja krytyczna
W teorii programowania współbieżnego (gdzie różne procesy lub wątki korzystają ze wspólnych zasobów) obiekt, z którego może korzystać wiele procesów lub wątków w sposób wyłączny nazywa się
zasobem dzielonym
(np. łazienka, drukarka), natomiast fragment wątku, w którym korzysta on z zasobu dzielonego nazywa się
sekcją krytyczną
(np. mycie się, drukowanie).
W danej chwili z obiektu dzielonego może korzystać tylko jeden wątek wykonując sekcję krytyczną uniemożliwia on wykonanie sekcji krytycznych innym wątkom.
Wzajemne wykluczanie
Wzajemne wykluczanie definiuje się tak by zsynchronizować wątki by każdy zajmował się własnymi sprawami i wykonywał sekcję krytyczną w taki sposób aby nie pokrywało się to z wykonaniem sekcji krytycznej innych wątków.
Aby to zrealizować należy do funkcji każdego wątku dodać dodatkowe instrukcję poprzedzające i następujące po sekcji krytycznej. Realizujemy czekanie na wolną łazienkę i zasadę uprzejmości.
Wymagania czasowe
Projektując wzajemne wykluczanie musimy uwzględnić kilka ważnych aspektów: Żaden wątek nie może wykonywać swej sekcji krytycznej nieskończenie długo, nie może się w niej zapętlić lub zakończyć w wyniku jakiegoś błędu (jeśli ktoś umrze w łazience to będziemy czekać nieskończenie długo jeśli nie sprawdzimy co się stało) Ważna zasada jest taka by w sekcji krytycznej wątek przebywał jak najkrócej i nie miał możliwości zakończenia się błędem.
Blokada
W pewnych sytuacjach wątek będzie wstrzymywany w oczekiwaniu na sygnał od innego wątku (gdy łazienka się zwolni) Blokada występuje wtedy gdy zbiór procesów jest wstrzymany w oczekiwaniu na zdarzenie, które może być spowodowane tylko przez jakiś inny proces z tego zbioru. Jaś jest w łazience i czeka aż zwolni się komputer a Małgosia siedzi przed komputerem w oczekiwaniu na zwolnienie się łazienki
Zagłodzenie
Jeśli sygnał synchronizujący może być odebrany tylko przez jeden wątek czekający to trzeba któryś wybrać Oznacza to, że wątek o niskim priorytecie może się zagłodzić gdyż ciągle będą wybierane wątki o wyższym priorytecie W systemie Windows jak już wiemy omijanie zagłodzenia jest realizowane poprzez dynamiczne podwyższanie priorytetów.
Problem producenta i konsumenta
Problem ten polega na zsynchronizowaniu dwóch wątków: producenta, który cyklicznie produkuje jakąś informację i przekazuje ją do konsumpcji i konsumenta, który cyklicznie pobiera tą informację i konsumuje ją. Informacje powinny być konsumowane w kolejności wyprodukowania.
Problem czytelników i pisarzy
Problem polega na zsynchronizowaniu dwóch grup cyklicznych wątków konkurujących o dostęp do jednej czytelni. Wątek czytelnik co jakiś czas odczytuje informacje z czytelni (może to robić z innymi czytelnikami) natomiast wątek pisarz co jakiś czas zapisuje nową informację i wówczas musi przebywać sam w czytelni. Rozwiązanie z możliwością zagłodzenia pisarzy (pisarz może wejść gdy czytelnia jest pusta) Rozwiązanie z możliwością zagłodzenia czytelników (jeśli pisarz chce pisać to należy mu to jak najszybciej umożliwić) a więc nowi czytelnicy nie wchodzą a pisarz czeka, aż czytelnicy siedzący w czytelni ją opuszczą
Problem pięciu filozofów
Problem polega na zsynchronizowaniu działań pięciu chińskich filozofów, którzy siedzą przy okrągłym stole i myślą. Jednak od czasu do czasu każdy filozof głodnieje i aby móc dalej myśleć musi się pożywić. Przed każdym filozofem stoi miska z ryżem, a pomiędzy dwoma miskami stoi jedna pałeczka. Podnosząc obie pałeczki filozof uniemożliwia jedzenie sąsiadom. Zakłada się, że jeżeli filozof podniósł pałeczki to w skończonym czasie się naje i odłoży je na miejsce. Rozwiązanie z możliwością blokady (głodny filozof czeka aż będzie wolna lewa pałeczka i ją podnosi, a następnie czeka, aż będzie wolna prawa pałeczka, podnosi ją i je, co się stanie gdy każdy chwyci lewą pałeczkę? ) Rozwiązanie z możliwością zagłodzenia (głodny filozof czeka aż obie pałeczki będą wolne, wtedy je podnosi i je, jeden filozof może siedzieć pomiędzy takimi, że zawsze któryś z nich będzie jadł)
Mechanizmy synchronizacji w WinAPI
Zdarzenia Mutexy Semafory Sekcje krytyczne Zegary oczekujące
Zdarzenia
Możemy sobie zdefiniować własne zdarzenie dzięki funkcji CreateEvent(). Zdarzenie raz zgłoszone istnieje w systemie do momentu odwołania. Każdy oczekujący watek widzi to zdarzenie jako dwustanową flagę (zgłoszone lub odwołane). Zgłaszamy zdarzenie funkcją SetEvent() i wszystkie wątki oczekujące mogą wznowić działanie. Funkcją ResetEvent() odwołujemy zdarzenie. Czekanie realizujemy za pomocą funkcji WaitForSingleObject(); W CreateEvent podajemy flagę ręcznego odwołania: TRUE wymaga abyśmy użyli ResetEvent() natomiast FALSE sprawia, że po przepuszczeniu wątku zdarzenie zostaje automatycznie odwołane. Od tej flagi zależy również działanie funkcji PulseEvent(), jeśli jest TRUE to funkcja PulseEvent przepuszcza wszystkie wątki oczekujące w danej chwili na zdarzenie, a jeśli FALSE to PulseEvent przepuszcza jeden z wątków oczekujących po czym odwołuje zdarzenie.
Zdarzenia
W przykładzie opisującym zdarzenia utworzymy wątek, który będzie czekał na otwarcie pliku i wtedy wykona swoją operację.
Po utworzeniu pliku wątek główny będzie czekał aż wątek do czytania wykona swoją operację.
Warto wspomnieć, że jeśli chcemy zaczekać na kilka zdarzeń, możemy użyć funkcji WaitForMultipleObjects();
Mutexy
Mutexy służą do realizacji wzajemnego wykluczania się. Stan mutexa jest ustawiany na sygnalizowany (kiedy żaden wątek nie sprawuje nad nim kontroli) lub niesygnalizowany (kiedy jakiś wątek sprawuje nad nim kontrolę). Każdy wątek czeka na objęcie mutexa w posiadanie zaś po zakończeniu operacji wymagającej wyłączności wątek uwalnia mutexa.
Mutexy
W celu stworzenia mutexa wywołujemy funkcję CreateMutex(). Wątek, który stworzył mutexa rząda natychmiastowego prawa do własności mutexa. Inne wątki (lub procesy) otwierają mutexa za pomocą funkcji OpenMutex(). Potem czekają na objęcie mutexa w posiadanie. Aby uwolnić mutexa wywołamy funkcję ReleaseMutex().
Jeśli wątek zakończy się i nie uwolni mutexa to taki mutex uważa się za porzucony. Każdy czekający wątek może objąć takiego mutexa w posiadanie. Na mutexa czekamy oczywiście funkcją WaitForSingleObject()
Mutexy
W przykładzie pokazującym działanie mutexów pokażemy, jak poszczególne wątki będą inkrementowały bądź dekrementowały zmienną globalną. Sekcją krytyczną będzie operacja inkrementacji lub dekrementacji i wypisywanie stosownej informacji na ekranie. Ujrzymy przy tym, że w czasie gdy jeden wątek wykonuje swoją sekcję krytyczną to inne wątki sekcji krytycznej objętej tym mutexem nie wykonują.
Semafory
Semafory to narzędzie służące do kontroli ilości wątków korzystających z zasobu. Za pomocą semaforu aplikacja może kontrolować np. maksymalną ilość otwartych plików. Semafory są dość podobne do mutexów. Nowy semafor tworzony funkcją CreateSemaphore(). Wątek tworzący semafor ustala wartość wstępną i maksymalną licznika. Inne wątki uzyskują dostęp do semafora za pomocą funkcji OpenSemaphore() i czekają na wejście za pomocą funkcji WaitForSingleObject(). Po zakończeniu sekcji krytycznej uwalnia się semafor za pomocą funkcji ReleaseSemaphore(). Wątki nie wchodzą w posiadanie semaforu tak jak to było z mutexem. Jeśli wątek, który stworzył mutex żąda do niego dostępu to otrzymuje go natychmiast. Z semaforem jest inaczej tzn. wątek, który stworzył semafor czeka w kolejce jak każdy inny wątek.
Zasada działania semafora
Inicjalizacja licznika i jego maksymalna wartość Kiedy licznik jest większy od 0 pierwszy w kolejce wątek może wejść do swojej sekcji krytycznej Kiedy wątek wchodzi do swojej sekcji krytycznej zmniejsza o 1 wartość licznika Jeśli licznik nie jest równy 0 to inny wątek również może go podnieść i korzystać z sekcji krytycznej Kiedy wątek opuszcza sekcję krytyczną i zwalnia semafor, wartość licznika zostaje podwyższona o 1. Dzięki temu kolejny wątek może wejść do swojej sekcji krytycznej.
Gdy z zasobu może korzystać tylko jeden wątek wtedy tworzymy tzw. semafor binarny, inicjalizujemy licznik na 1, a wartość maksymalną ustawiamy na 1.
Semafory
W funkcji CreateSemaphore podajemy następujące argumenty: LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, wskaznik do atrybutu ochrony (my podajemy NULL) LONG lInitialCount, wartość początkowa licznika LONG lMaximumCount, maksymalny licznik LPCTSTR lpName, wskaźnik do nazwy semafora Funkcja zwraca uchwyt do semafora Gdy wątek kończy działanie sekcji krytycznej wywołuje funkcję ReleaseSemaphore() podając w argumentach: uchwyt do semafora, zwolniony licznik i wskaźnik do poprzedniego licznika, (NULL jeśli nie jest to potrzebne).
Semafor i rozwiązanie problemu pięciu filozofów
Za pomocą semaforów rozwiążemy ten problem korzystając z rozwiązania z możliwością blokady Paleczka[i] będzie reprezentować semafor pałeczki o numerze i. Podniesienie pałeczki będzie to korzystanie z i-tego semafora, a skończenie jedzenie będzie to jego zwolnienie.
Sekcje krytyczne
Narzędzie systemu Windows tylko do obsługi współbieżności wątków (w odróżnieniu od zdarzeń, mutexów i semaforów). Umożliwia implementację sekcji krytycznej Najczęściej stosowana metoda w synchronizacji wątków w Windows
Sekcje krytyczne
Tworzymy zmienną typu CRITICAL_SECTION W każdej z poniższych funkcji jako argument podajemy adres zmiennej którą utwożylismy Inicjalizujemy sekcję krytyczną:
VOID InitializeCriticalSection();
Wejście do sekcji krytycznej:
VOID EnterCriticalSection();
Opuszczenie sekcji krytycznej
: VOID LeaveCriticalSection();
Zamknięcie sekcji krytycznej:
VOID DeleteCriticalSection();
W Windows NT gdy chcemy wejść do sekcji krytycznej mamy możliwość użycia funkcji:
BOOL TryEnterCriticalSection();
Sekcje krytyczne przykład
W przykładowej aplikacji znów posłużymy się synchronizacją wyświetlania informacji na ekran.
Utworzymy 2 sekcje krytyczne by pokazać, że mechanizm CRITICAL_SECTION ułatwia tworzenie wielu sekcji krytycznych i łatwiej nad nimi panować bo funkcję wymagają tylko adresu zmiennej.
Pierwsza sekcja krytyczna będzie służyć operacji na globalnej zmiennej a druga będzie służyć wyświetlaniu informacji o stanie tej zmiennej.
Zegary oczekujące
Zegary umożliwiają głównie regularne wywoływanie wątków.
Zegar oczekujący przechodzi w stan sygnalizowany po upływie określonego czasu lub w określonych odstępach czasu z automatycznym powrotem do stanu niesygnalizowanego.
Zegar oczekujący tworzymy funkcją:
CreateWaitableTimer(),
której argumenty to: wskaźnik na strukturę SA, zmienna BOOL mówiąca o tym czy zegar ustawiamy ręcznie (TRUE) czy automatycznie (FALSE), nazwa zegara. Funkcja zwraca uchwyt do zegara.
Zegary oczekujące
Po utworzeniu zegar znajduje się w stanie nie aktywnym i nie sygnalizowanym Aktywujemy zegar funkcją
SetWaitableTimer()
z parametrami: Uchwyt do zegara czas po którym zegar przejdzie w czas sygnalizowany (konkretna data i godzina w formacie FILETIME) okres czasu (w milisekundach) po upływie którego zegar przechodzi w stan sygnalizowany, zegar automatycznie jest uruchamiany co podany okres, aż do wywołania funkcji
CancelWaitableTimer()
Adres funkcji wywołania zwrotnego Wskaźnik do przekazywanej struktury Zmienna typu BOOL, która gdy ma wartość TRUE powoduje, że system budzi się z trybu oszczędzania energii.
Zegary oczekujące
Dostęp do zegara wymaga funkcji
OpenWaitableTimer()
do której podajemy zakres dostępu, przełącznik dziedziczenia uchwytu i nazwę zegara.
POSIX thread
pthread jest to najpopularniejsza biblioteka służąca do implementacji wątków wchodząca w skład standardu POSIX Umożliwia implementację zarówno w systemach UNIX, Linux, a także w Windows. Interfejs jest zaprojektowany obiektowo pthread umożliwia: Tworzenie wątków Synchroniczne kończenie wątków Lokalne dane wątku Obsługę mutexów Funkcje oczekujące Ustalanie priorytetów Ograniczenia czasowe na zajście niektórych zdarzeń
POSIX thread
Tworzenie wątku:
int pthread_create( pthread_t *id, const pthread_attr_t *attr, void* (fun*)(void*), void* arg)
id identyfikator wątku; attr wskaźnik na atrybuty wątku, określające szczegóły dotyczące wątku; można podać NULL, wówczas zostaną użyte domyślne wartości; fun – adres funkcji wykonywania wątku; przyjmuje argument typu void* i zwraca wartość tego samego typu; arg - przekazywany do funkcji.
Boost Thread
Zapewnia możliwość programowania wielowątkowego w jednolity sposób na różnych systemach Zawiera klasy i funkcje do zarządzania wątkami i do synchronizacji (m.in. Klasa thread i thread_group) Wnikliwy opis biblioteki na: http://www.boost.org/doc/html/thread.html
TThread
C++ Builder w bibliotece VCL umożliwia korzytsanie z klasy TThread W konstruktorze klasy TThread podajemy flagę CREATE_SUSPENDED jako zmienną typu bool C++ Builder ma też gotowe klasy zdarzeń (TEvent), sekcji krytycznej (TCriticalSection, a także listy wątków (TThreadList).
Jest także specjalna klasa rozwiązująca problem czytelników i pisarzy (TMultiReadExclusiveWriteSynchronizer), w której mamy metody do rozpoczęcia i kończenia czytania i pisania
Thread
Na platformie .NET korzysta się z klasy Thread.
W C# dołączamy using System.Threading; Tworzymy swoją klasę i publiczną metodę obsługi wątku i podajemy ją w argumencie konstruktora klasy Thread jako argument funkcji ThreadStart Np.
Thread watek = new Thread(ThreadStart(moja_metoda));
Warto zajrzeć
Wątki i synchronizacja w WinAPI: http://win32prog.republika.pl/ebook/watki.pdf
http://student.eldoras.com/UJ/programowanie_rozproszone_i _rownolegle/prr_w4.pdf
http://msdn.microsoft.com/en us/library/ms682453%28VS.85%29.aspx
Inne biblioteki: http://msdn.microsoft.com/en us/library/system.threading.thread.aspx
http://www.boost.org/doc/libs/1_45_0/doc/html/thread.html
https://computing.llnl.gov/tutorials/pthreads/
Literatura
Literatura dotycząca programowania współbieżnego: Silberschatz, Peterson, Galvin
„Podstawy systemów operacyjnych”
Zbigniew Weiss, Tadeusz Gruźlewski
„Programowanie współbieżne i rozproszone”
KONIEC
Dziękuję za uwagę