Wątki i ich synchronizacja

Download Report

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ę