Funkcje API 2000. Komunikatory, semafory, wątki
Co to są funkcje API?
API czyli z angielskiego „application programming interface”, a po polsku: interfejs programowania aplikacji. Dawniej jedynymi użytkownikami komputerów byli informatycy i programiści. W tamtych czasach program komputerowy pobierał dane, przeliczał je i drukował wynik. Z biegiem czasu zaczęto jednak tworzyć w programach menu, które umożliwiało użytkownikowi dostęp do wszystkich opcji programu, a przy tym użytkownik nie musiał znać zawiłych komend. Gdyby każdy z programistów projektował własny interfejs, to po pierwsze: pisanie programu zajęło by więcej czasu, po drugie każdy program wyglądałby inaczej. Dlatego autorzy systemu operacyjnego Windows stworzyli biblioteki funkcji zwane API Windows, które pozwalają zbudować praktycznie dowolną aplikację działającą w systemach Windows 95, 98, NT, 2000, XP. API zawiera setki funkcji umożliwiających kontrolę, sterowanie wszystkimi zasobami, zaczynając od sterowania myszką, poprzez kontrolowanie procesów Windows, a kończąc na zarządzaniu pamięcią. Aplikacje, które używają funkcji API wymagają oczywiście, aby był zainstalowany jeden z wyżej wymienionych systemów. Nie wszystkie funkcje API działają na wszystkich systemach serii Windows, niektóre, zwłaszcza te najnowsze mogą wymagać Windows'a NT, 2000 lub XP. Oczywiście można się obejść bez API wykorzystując biblioteki obiektowe, takie jak MFC (Microsoft) czy OWL (Borland) lub jeszcze inne.
Proces.
Pierwsze systemy komputerowe umożliwiały wykonywanie tylko jednego programu w danej chwili. Program taki miał nadzór nad całym systemem, a oprócz tego korzystał ze wszystkich zasobów systemu. Współczesne systemy komputerowe pozwalają na umieszczenie w pamięci operacyjnej wielu programów i współbieżne ich wykonywanie. Te zmiany pociągnęły za sobą konieczność ostrzejszej kontroli większego odseparowania od siebie poszczególnych programów. Spowodowało to powstanie pojęcia procesu.
Przez proces rozumie się program będący w trakcie wykonywania. Proces jest jednostką pracy w nowoczesnym systemie z podziałem czasu. Z tego wynika fakt, że system składa się więc ze zbioru procesów:
procesy systemu operacyjnego - wykonują kod systemowy.
procesy użytkowe - działają według kodu należącego do użytkowników.
Wszystkie te procesy potencjalnie mogą być wykonywane współbieżnie dzięki podziałowi między nie mocy obliczeniowej procesora (lub procesorów). Przełączając procesor do poszczególnych procesów, system operacyjny może zwiększyć wydajność komputera.
Opis procesu :
Proces jest wykonywanym programem, wykonanie procesu musi przebiegać w sposób sekwencyjny. Wynikiem tego jest to, że w dowolnej chwili na zamówienie danego procesu może być wykonywany co najwyżej jeden rozkaz kodu programu.
Proces jest czymś więcej niż samym kodem programu (nazywa się go sekcją tekstu (ang. text section)). W pojęciu procesu mieści się również bieżąca czynność reprezentowana przez wartość licznika rozkazów (ang. program counter) oraz zawartość rejestrów procesora.
Licznik rozkazów - wskazuje adres następnego rozkazu do wykonania w procesie.
Rejestry procesora - liczba i typy rejestrów zależą od architektury komputera, wyróżniamy : akumulatory, rejestry indeksowe, wskaźniki stosu, rejestry ogólnego przeznaczenia i rejestry warunków. Informacje o stanie tych rejestrów muszą być przechowywane w czasie przerwań po to by proces mógł później być poprawnie kontynuowany.
Do procesu na ogół należy także: stos procesu (ang. process .stack), który przechowuje dane tymczasowe (takie jak parametry procedur, adresy powrotne i zmienne tymczasowe) oraz sekcja danych (ang. data section) zawierająca zmienne globalne.
Program sam w sobie nie jest procesem. Program jest obiektem pasywnym. Natomiast proces jest obiektem aktywnym, z licznikiem rozkazów określającym następny rozkaz do wykonania i ze zbiorem przydzielonych mu zasobów.
Dwa procesy mogą być związane z jednym programem i tak będą one zawsze traktowane jako dwie oddzielne sekwencje wykonania. Przykładem może być : użytkownik może zapoczątkować pracę wielu kopii edytora. W każdym z tych przypadków mamy do czynienia z osobnymi procesami, które - niezależnie od równoważności sekcji tekstu - będą się różniły sekcjami danych, a ponad to każdy wykonywany proces może uruchomić wiele nowych procesów.
Stany procesu :
Każdy wykonywany proces jest w pewnym stanie, który może zmieniać w zależności od bieżącej czynności procesu. Mogą to być następujące stany procesów :
nowy - proces został utworzony.
aktywny - są wykonywane instrukcje.
oczekiwanie - proces czeka na wystąpienie jakiegoś zdarzenia (np. zakończenie operacji wejścia/wyjścia).
gotowy - proces czeka na przydział procesora.
zakończony - proces zakończył działanie.
Wątek.
Wątek (ang. thread), nazywany także procesem lekkim (ang. lightweight process - LWP). Jest podstawową jednostką wykorzystania procesora, w skład której wchodzą : licznik rozkazów, zbiór rejestrów i obszar stosu. Wątek współużytkuje wraz z innymi równorzędnymi wątkami sekcję kodu, sekcję danych oraz takie zasoby systemu operacyjnego, jak otwarte pliki i sygnały, co łącznie określa się jako = zadanie (ang. task).
Proces tradycyjny, czyli ciężki (ang. heavyweight), jest równoważny zadaniu z jednym wątkiem. Zadanie nie robi nic, jeśli nie ma w nim ani jednego wątku. Natomiast wątek może przebiegać w dokładnie jednym zadaniu. Daleko posunięte dzielenie zasobów powoduje, że przełączanie procesora między równorzędnymi wątkami, jak również tworzenie wątków, jest tanie w porównaniu z przełączaniem kontekstu między tradycyjnymi procesami ciężkimi. Choć przełączanie kontekstu między wątkami nadal wymaga przełączania zbioru rejestrów, jednak nie trzeba wykonywać żadnych prac związanych z zarządzaniem pamięcią. Jak w każdym środowisku przetwarzania równoległego, podział procesu na wątki może prowadzić do zagadnień sterowania współbieżnością i konieczności stosowania sekcji krytycznych lub zamków.
W niektórych systemach zrealizowano też wątki poziomu użytkownika (ang. user - level threads), z których korzysta się za pośrednictwem wywołań bibliotecznych zamiast odwołań do systemu. dzięki czemu przełączanie wątków nie wymaga wzywania systemu operacyjnego i przerwań związanych z przechodzeniem do jego jądra. Przełączanie między wątkami poziomu użytkownika może być wykonywane niezależnie od systemu operacyjnego, może więc się odbywać bardzo szybko. Blokowanie jednego wątku i przełączanie do innego wątku jest zatem rozsądnym rozwiązaniem problemu wydajnego obsługiwania przez serwer wielu zamówień. Wątki poziomu użytkownika mają jednak też swoje wady. Na przykład, jeśli jądro jest jednowątkowe, to każdy wątek poziomu użytkownika odwołujący się do systemu będzie powodował oczekiwanie całego zadania na zakończenie wywołania systemowego.
Rodzaje wątków :
Wątek jądrowy - ma jedynie małą strukturę danych i stos. Przełączanie wątków jądrowych nie wymaga zmiany informacji dotyczących dostępu do pamięci, jest więc stosunkowo szybkie.
Proces lekki (LWP) - zawiera blok kontrolny procesu z danymi rejestrowymi, informacjami rozliczeniowymi i informacjami dotyczącymi pamięci. Przełączanie procesów lekkich wymaga więcej pracy i jest dość wolne.
Wątek poziomu użytkownika - wymaga tylko stosu i licznika rozkazów, nie są mu potrzebne żadne zasoby jądra. Jądro nie jest angażowane w planowanie wątków poziomu użytkownika, więc ich przełączanie jest szybkie. Mogą istnieć tysiące wątków poziomu użytkownika, a jedyne, co będzie widoczne dla jądra, to procesy lekkie w procesie realizującym wątki tego poziomu.
Funkcje związane z wątkami :
CreateThread( )
ResumeThread( )
SuspendedThread( )
Sleep( )
ExitThread( )
WaitForSingle0bject ( ) - funkcja oczekująca na stan sygnalny
WaitForMultipleObjects ( ) - funkcja dzięki której wątek oczekuje na przejście wielu obiektów w stan sygnalny.
Function WaitForSignaleObject (hHandle : THandle;
DwMilliseconds : DWOR) : DWOR; stdcall;
Pierwszy parametr hHandle zawiera uchwyt do obiektu, na który czeka, następnie drugi określa długość czasu oczekiwania w milisekundach (wartość tego parametru INFINITE - nieskończoność).
Tworzenie wątku :
W systemie Windows 95/98/NT/2000 i XP w ramach procesu można utworzyć kilka wątków, wykonywanych współbieżnie, mających dostęp do wszelkich zasobów procesu. Każdy proces posiada co najmniej jeden wątek zwany wątkiem główny. Dodatkowo, można dla niego tworzyć wątki poboczne, a służy do tego funkcja API, o nazwie CreateThread( ). W Delphi do dyspozycji jest funkcja BeginThread( ), w której jest wywołanie wcześniej wspomnianej funkcji API.
Wzór odpowiedniej deklaracji :
function BeginThread ( SecurityAttributes : Pointer;
StackSize : LongWord;
ThreadFunc : TThreadFunc;
Parameter : Pointer;
CreationFlags : LongWord;
var ThreadId : LongWord) : Integer;
Wyjaśnienie kolejnych parametrów :
SecurityAttributes - (wskazanie na atrybuty bezpieczeństwa) - zawiera wskaźnik do rekordu opisującego dostęp do uchwytu wątku, może on zawierać wskaźnik pusty NULL(w delphi Nil).
StackSize - (rozmiar stosu w Bajtach) - określa wielkość stosu dla nowego wątku. Wpisanie wartości 0 spowoduje użycie wielkości domyślnej.
ThreadFunc - (wskazanie na funkcje będącą ciałem wątku) - parametr zawiera adres funkcji, w której zawarty jest kod tworzonego wątku.
Parameter - (atrybuty tworzonego wątku) - ten parametr zawiera wartość, która będzie użyta w wywołaniu funkcji wątku.
CreationFlags - (atrybuty tworzonego wątku) - określa sposób utworzenia wątku, może on przyjąć jedną z dwóch wartości :
CREATE_SUSPENDED - oznacza to że po utworzeniu wątku jego wykonanie zostanie zawieszone.
0 - wątek zostanie utworzony w zwykły sposób(tzn. będzie mógł działać).
ThreadId - parametr do którego będzie przypisane wskazanie na zmienną, do której zostanie zapisany identyfikator wątku, nadany mu w chwili utworzenia.
Po poprawnym utworzeniu wątku przez funkcję CreateThread( ), zwraca ona uchwyt wątku, natomiast w razie nie powodzenia zwraca ona wartość 0.
Użycie w argumencie CreationFlags wartości CREATE_SUSPENDED, powoduje że wątek zostaje utworzony ale pozostaje w stanie zawieszenia (tzn. procesor nie przełączy się na to zadanie i nie będzie go wykonywał). Obiekt, który reprezentuje wątek, jest wyposażony w tzw. licznik zawieszeń. Gdy w liczniku zawieszeń znajduję się wartość większa od zera to wątek pozostaje w stanie zawieszenia. By dany wątek został wykonany należy uruchomić funkcję ResumeThread( ), z uchwytem wątku przekazywanym w parametrze. Uruchomienie funkcji ResumeThread( ) powoduje zmniejszenie o jeden wartości licznika zawieszeń. Gdy wartość tego licznika osiągnie zero wątek ulegnie wykonaniu.
Funkcja, która powoduje ponowne zawieszenie wątku to SuspendThread( ). Wywołanie tej funkcji kilka razy powoduje za każdym razem zwiększenie licznika zawieszeń o jeden, co powoduje że jeśli chcemy wykonać wątek musimy funkcje ResumeThread( ) wykonać też kilka razy aż do momentu gdy licznik zawieszeń się wyzeruje.
Kolejną z funkcji dotyczących wątków jest funkcja Sleep( ), która przyjmuje jako parametr wartość w milisekundach. Liczba będąca parametrem tej funkcji wskazuje jak długą chwilę należy zawiesić wykonywanie bieżącego wątku.
Tradycyjnie zakończenie wątku następuj po zakończeniu wykonywania jego funkcji. Zanim to jednak nastąpi można wymusić to, by wątek się zakończył. Funkcją API, która do tego celu służy jest ExitThread( ).
Priorytety wątków :
Ważną sprawą są priorytety wątków, określają one ich ważność, a co za tym idzie ilość przydzielanego im czasu procesora. Priorytet każdego wątku jest ściśle powiązany z priorytetem procesu, do którego należy, tak więc określa się go w sposób względny. Do nadawania wątkowi priorytetu służy funkcja SetThreadPriority( ). Deklaracja tej funkcji wygląda następująco :
function SetThreadPriority (hThread : Thandle;
nPriority : integer) : Bool;
Parametr pierwszy ( hThread ) określa uchwyt wątku, natomiast drugi parametr (nPriority) zawiera wartość nadawanego mu priorytetu.
Lista wartości, jakie może przyjmować drugi parametr :
THREAD_PRIORITY_IDLE - priorytet równy 16 dla procesów o najwyższym priorytecie (należących do klasy priorytetowej czasu rzeczywistego) oraz 1 dla pozostałych procesów.
THREAD_PRIORITY_LOWEST - priorytet wątku mniejszy o 2 od priorytetu procesu.
THREAD_PRIORITY_BELOW_NORMAL - priorytet wątku mniejszy o 1od priorytetu procesu.
THREAD_PRIORITY_NORMAL - priorytet wątku równy priorytetowi jego procesu.
THREAD_PRIORITY_ABOVE_NORMAL - priorytet wątku większy o 1 od priorytetu procesu.
THREAD_PRIORITY_HIGHEST - priorytet wątku większy o 2 od priorytetu procesu.
THREAD_PRIORITY_TIME_CRITICAL - priorytet równy 31 dla procesów o najwyższym priorytecie (należących do klasy priorytetowej czasu rzeczywistego) oraz 15 dla pozostałych procesów.
Nie tylko wysokość priorytetu decyduje o kolejności i sposobie wykonywania wątków, dlatego nie należy nadawać priorytetu wątkom niepotrzebnie, gdyż w większości przypadków, najwłaściwszym rozwiązaniem jest wartość domyślna, przyznawana im w sposób automatyczny. Nie należy ustawiać bardzo wysokiego priorytetu dla wątku, który wykonuje pracochłonne zadanie, bo może to spowodować wyraźne obniżenie wydajności całego systemu. Wątkami kwalifikującymi się do otrzymania wysokiego priorytetu, są taki które wykonują swoje czynności rzadko i krótko, a dodatkowo znaczenie ma szybkość reakcji na określone zdarzenie (przykładem takich wątków mogą być procesy obsługujące dialog z użytkownikiem - odpowiedź na naciśnięcie przycisku myszki, czy klawisza na klwiaturze).
Zmienne wątków :
Gdy w programie działała kilka wątków, to korzystają one z tej samej przestrzeni adresowej, czego wynikiem jest fakt , że mają one dostęp do tych samych zmiennych globalnych. Natomiast zmienne lokalne funkcji lub procedur widoczne są jedynie w tych podprogramach, ponieważ są definiowane na stosie. Zdarza się często, że są potrzebne zmienne globalne, a co za tym idzie widziane przez wiele funkcji równocześnie, jednak odrębne dla każdego wątku. W tym wypadku system operacyjny daje nam możliwość zdefiniowania takich zmiennych w tzw. pamięci lokalnej wątku (TLS - ang. Thread Local Storage). W Delphi do zdefiniowania tego typu zmiennych globalnych służy słowo kluczowe threadvar zamiast var.
Thread Local Storage - czyli pamięć lokalna wątku. Realizowana jest w prywatnym obszarze stosu wątku, w którym na ten cel rezerwowane są 64 wskaźniki. Z każdą zmienną globalną wątku związany jest jeden z tych wskaźników, co powoduje iż każdy wątek posiada w rzeczywistości dostęp do swojej własnej, odrębnej zmiennej. Dostęp do tych zmiennych jest nieco wolniejszy niż odwołanie do „zwykłych zmiennych”, a realizowany jest za pomocą odpowiednich funkcji API.
Uwagi na temat implementacji wątków w systemie operacyjnym :
Pakiet wątków : zbiór elementarnych działań na wątkach dostępnych w systemie (np. procedur bibliotecznych.
Implementacja wątków w przestrzeni użytkownika :
jądro nie wie o wątkach, widzi tylko jednowątkowe procesy.
Zalety :
można używać wątków w systemie, który ich nie implementuje (np. pierwotnie UNIX).
możliwe szybkie przełączanie wątków - tylko przeładowania wskaźników stosu i instrukcji oraz rejestrów (najszybsze działania w systemie komputerowym).
każdy proces może używać własnego algorytmu planowania dla swoich wątków.
Wady :
przy blokowanych odwołaniach wątków do systemu - proces nie może oddać sterowania systemowi, musi czekać na swoje wątki. Używa się kodu sprawdzającego czy odwołania wątków będą blokować. Tych wątków używa się głownie w zadaniach z blokującymi odwołaniami, gdzie mają poprawić wydajność.
nie ma wywłaszczania, wątki muszą same oddawać sterowanie procedurze wykonawczej procesu.
Implementacja wątków w jądrze :
system wykonawczy jest częścią jądra.
jądro zakłada tablicę wątków dla każdego procesu.
wszystkie funkcje mogące blokować mają postać odwołań do systemu.
gdy wątek czeka, jądro wybiera następny - wydajność.
Wady :
koszt odwołań do systemu (duży czas).
Ogólne problemy (najtrudniejsze do implementacji) z wątkami :
obsługa przerwań (sygnałów).
niewznawialne procedury systemowe.
***SEMAFORY*****
Synchronizowanie procesów :
Proces współpracujący, może wpływać na inne procesy w systemie lub podlegać ich oddziaływaniom. Procesy współpracujące mogą bezpośrednio dzielić logiczną przestrzeń adresową - ( tzn. zarówno kod, jak i dane) albo zezwala się im na dzielenie danych tylko za pośrednictwem plików. Pierwszą możliwość osiąga się za pomocą procesów lekkich, czyli wątków. Współbieżny dostęp do danych dzielonych może powodować ich niespójność.
Problem sekcji krytycznej:
Sekcja krytyczna - system złożony jest zazwyczaj z n procesów. Każdy proces ma segment kodu zwany sekcja krytyczną (ang. critical section), w którym może zmieniać wspólne zmienne, aktualizować tablice, pisać do pliku itd. Ważną cechą tego systemu jest to, że kiedy jeden proces wykonuje sekcję krytyczną, wówczas żaden inny proces nie jest dopuszczony do wykonywania swojej sekcji krytycznej. Zatem wykonanie sekcji krytycznych przez procesy podlega wzajemnemu wykluczaniu (wzajemnemu wyłączaniu: ang. mutual exclusion) w czasie. Problem sekcji krytycznej polega na skonstruowaniu protokołu, który mógłby posłużyć do organizowania współpracy procesów. Każdy proces musi prosić o pozwolenie na wejście do swojej sekcji krytycznej. Fragment kodu realizującego taką prośbę nazywa się sekcją wejściową (ang. entry section). Po sekcji krytycznej może występować sekcja wyjściowa (ang. exit section). Pozostały kod nazywa się resztą (ang. remainder section).
Rozwiązanie problemu sekcji krytycznej musi spełniać następujące trzy warunki:
Wzajemne wykluczanie : jeśli jakiś proces, działa w swojej sekcji krytycznej, to żaden inny proces nie działa w sekcji krytycznej.
Postęp : jeśli żaden proces nie działa w sekcji krytycznej oraz istnieją procesy, które chcą wejść do sekcji krytycznych, to tylko procesy nie wykonujące swoich reszt mogą kandydować jako następne do wejścia do sekcji krytycznych i wybór ten nie może być odwlekany w nieskończoność.
Ograniczone czekanie : musi istnieć wartość graniczna liczby wejść innych procesów do ich sekcji krytycznych po tym, gdy dany proces zgłosił chęć wejścia do swojej sekcji krytycznej i zanim uzyskał na to pozwolenie.
Zakłada się że proces wykonywany jest z prędkością niezerową. Natomiast nie musimy niczego zakładać o względnej szybkości każdego z n procesów.
W środowisku jednoprocesorowym problem sekcji krytycznej daje się łatwo rozwiązać, jeśli można zakazać występowania przerwań podczas podczas modyfikowania zmiennej dzielonej. W ten sposób uzyskuje się pewność, że dany ciąg rozkazów zostanie wykonany sekwencyjnie, bez wywłaszczenia. Ponieważ wykonanie żądanego innego rozkazu nie jest możliwe, więc nie nastąpi żadna nieoczekiwana zmiana wspólnej zmiennej.
Niestety, rozwiązanie to nie jest przydatne w środowisku wieloprocesorowym. Wyłączanie przerwań w wieloprocesorze może być czasochłonne, gdyż wymaga przekazywania komunikatów do wszystkich procesorów. Owo przekazywanie komunikatów opóźnia wejście do każdej sekcji krytycznej, co powoduje spadek wydajności systemu. Należy tez zwrócić uwagę na skutki wywoływane w zegarze systemowym, jeśli jest on uaktualniany za pomocą przerwań.
Z tych powodów w wielu maszynach są specjalne rozkazy sprzętowe pozwalające w sposób niepodzielny sprawdzić i zmienić zawartość słowa albo zmienić zawartości dwu słów. Takie specjalne rozkazy mogą stosunkowo łatwo posłużyć do rozwiązania problemu sekcji krytycznej.
Rozwiązanie problemu sekcji krytycznej w sposób sprzętowy nie jest łatwe do uogólnienia w bardziej złożonych zagadnieniach. Do omijania tej trudności służy narzędzie synchronizacji zwane semaforem
Semafory - mogą być wykorzystywane tam, gdzie zasób dzielony jest na ograniczoną ilość użytkowników. Semafor działa jak furtka kontrolująca ilość wątków wykonujących jakiś fragment kodu. Za pomocą semaforów aplikacja może kontrolować na przykład maksymalną ilość otwartych plików, czy utworzonych okien. Semafory są w działaniu bardzo podobne do mutexów.
Semafory :
Narzędzie synchronizacji zwane semaforem (ang. semaphore). Semafor jest zmienną całkowitą, która - oprócz nadania wartości początkowej - dostępna jest tylko za pomocą dwóch standartowych, niepodzielnych operacji :
czekaj - z ang. wait.
sygnalizuj - z ang. signal.
Klasyczne definicje operacji czekaj i sygnalizuj są następujące :
czekaj (S) : while S
0 do nic;
S : = S - 1;
sygnalizuj(S) : S : = S + 1;
Zmiany wartości całkowitej semafora muszą być wykonywane za pomocą operacji czekaj i sygnalizuj w sposób niepodzielny. Oznacza to, że gdy jeden proces modyfikuje wartość semafora, wówczas żaden inny proces nie może jednocześnie zmieniać tej wartości. Dodatkowo w przypadku operacji czekaj(S) nie może wystąpić przerwanie podczas sprawdzania wartości zmiennej całkowitej S ( S
0) i jej ewentualnego zmieniania S : = S - 1
Zastosowanie i tworzenie semaforów :
Semafory można wykorzystać do synchronizacji dostępu do zasobów, dla których istnieje pewien limit równocześnie odwołujących się wątków.
Funkcje związane z semaforami :
CreateSemaphore ( )
ReleaseSemaphore ( )
WaitForSingle0bject ( )
Do tworzenia nowego semafora służy funkcja API :
Function CreateSemaphore ( lpSemaphoreAttributes : PsecurityAttributes;
lInitialCount, lMaximumCount : Longint;
lpName : Pchar) : Thandle; stdcall;
Pierwszy parametr lpSemaphoreAttributes - (wskazanie na atrybuty bezpieczeństwa) - zawiera wskaźnik do rekordu opisującego dostęp do uchwytu semafora. Po za tym funkcja ta posiada dwa charakterystyczne dla siebie parametry : lInitialCount oraz lMaximumCount, które są związane z licznikiem semafora. Licznik semafora decyduje o dopuszczalnej liczbie wątków, synchronizowanych za pomocą semafora. Pierwszy z nich określa początkową zawartość licznika, natomiast drugi jego wartość maksymalną.
Obiekt semafora znajduje się w stanie sygnalnym wówczas, kiedy jego licznik ma wartość większą od 0. Wtedy także wywołanie funkcji oczekującej na stan sygnalny semafora WaitForSingle0bject ( ), zmniejszy jego licznik o 1i zakończy się sukcesem (zwróci WAIT_OBJECT_0). Będzie to możliwe tylko do momentu wyzerowania licznika semafora. Taka sytuacja zajdzie wówczas, kiedy skorzysta z niego równocześnie maksymalna, dopuszczalna liczba wątków.
Zwolnienie semafora przez wątek następuje w wyniku wywołania funkcji API o nazwie ReleaseSemaphore ( ). Daje ona możliwość zwiększenia jego licznika o wartość większą, niż 1, co w pewnych sytuacjach może być przydatne.
Ostatnim parametrem jest lpName - jest nazwa semafora (dowolny ciąg znaków).
Klasyczne problemy synchronizacji.
Omawiane w tym punkcie problemy synchronizacji są wyznacznikiem podczas testowania nowo zaproponowanych schematów synchronizacji (każdy nowy schemat synchronizacji testuje się pod względem rozwiązania tych problemów). Omówimy teraz kilka różnych problemów synchronizacji do rozwiązania których posłużą semafory.
Problem czytelników i pisarzy.
Jest dany obiekt danych, który ma podlegać dzieleniu między kilka procesów współbieżnych. Niektóre z tych procesów będą tylko czytać zawartość naszego obiektu, ale będą też i takie, które będą go uaktualniać, to znaczy czytać i zapisywać. Aby rozróżnić te dwa procesy można pierwszy (zainteresowany tylko czytaniem pliku) nazwać czytelnikiem, natomiast pozostałe pisarzami. W tym przypadku jednoczesne korzystanie z obiektu przez kilku czytelników nie powoduje żadnych szkodliwych skutków. Natomiast problem wynikłby wtedy gdy między czytelnikami znalazłby się pisarz(kilku pisarzy) i także miałby dostęp do obiektu dzielonego, było by to powodem wielkiego chaosu.
By wyżej przedstawiony problem rozwiązać należy zagwarantować wyłączność dostępu pisarzy do obiektu dzielonego. Ten problem synchronizacji został nazwany problemem czytelników i pisarzy (ang. readers - writers problem). Odkąd sformułowano ten problem, jest on wykorzystywany do testowania wszystkich nowych elementów synchronizacji. Problem czytelników i pisarzy ma wiele odmian z zastosowaniem priorytetów:
Pierwszy problem czytelników i pisarzy, zakłada że żaden czytelnik nie powinien czekać na zakończenie pracy innych czytelników tylko z tego powodu, że czeka pisarz.
Drugi problem czytelników i pisarzy, zakłada że jeśli pisarz czeka na dostęp do obiektu dzielonego, to żaden nowy czytelnik nie rozpocznie czytania.
Każdy z dwóch wymienionych problemów powoduje głodzenie. Głodzenie (sytuacja, w której procesy czekają w nieskończoność pod semaforem) ograniczenie dostępu procesu do obiektu. W pierwszym przypadku głodzenie dotyczy pisarzy, natomiast w drugim czytelników.
W rozwiązaniu pierwszego problemu czytelników i pisarzy procesy czytelników dzielą poniższe zmienne :
var WW, S : semaphore;
liczba-czyt : integer;
Semafor WW i S są zainicjalizowane wartością 1, liczba-czyt ma wartość początkową 0. Semafor S jest wspólny dla procesów czytelników i pisarzy. Semafor WW służy do zagwarantowania wzajemnego wykluczania przy aktualizacji zmiennej liczba-czyt (która przechowuje liczbę procesów czytających obiekt. Semafor S organizuje wzajemne wykluczanie pracy pisarzy. Jest on też używany przez pierwszego wchodzącego lub ostatniego opuszczającego sekcję krytyczną czytelnika. Nie używają go czytelnicy wchodzący lub wychodzący w czasie gdy inni czytelnicy są w sekcjach krytycznych.
Struktura procesu pisarza :
czekaj(S);
...
w tym momencie pisarz wykonuje operacje na obiekcie dzielonym.
...
sygnalizuj(S);
Struktura procesu czytelnika :
czekaj(WW);
liczba-czyt : =liczba-czyt + 1;
if liczba-czyt = 1 then czekaj (S);
sygnalizuj(WW);
...
tu następuje czytanie
...
czekaj(WW);
liczba-czyt : =liczba-czyt - 1;
if liczba-czyt = 0 then sygnalizuj (S);
sygnalizuj(WW);
Gdy pisarz przebywa w sekcji krytycznej to oczekuje na niego n czytelników, z czego 1 do semafora S, a n-1 ustawia się w kolejce do semafora WW.
Problem posilających się filozofów.
Na początek wyobraźmy sobie pięciu filozofów, którzy całe życie spędzają na rozmyślaniu i jedzeniu. Filozofowie dzielą okrągły stół, wokół którego ustawiono 5 krzeseł (po jednym dla filozofa) oraz na środku stołu stoi miska ryżu, a naokoło leży pięć pałeczek (rys. poniżej). Kiedy dany filozof myśli, wtedy nie kontaktuje się ze swoimi kolegami. Czasami filozof odczuwa głód i wtedy siada do stołu i próbuje wziąć do rąk dwie pałeczki, leżące najbliżej jego miejsca (pałeczki znajdujące się między nim a sąsiadami z lewej i z prawej). Za każdym razem filozof może podnieść tylko jedną pałeczkę, bo jest oczywiste, że nie będzie wyrywać pałeczki z ręki sąsiada. Gdy filozof zdobędzie dwie pałeczki to rozpoczyna jedzenie i nie rozstaje się z nimi ani na chwilę dopóki, nie skończy jeść. Po jedzeniu filozof odkłada pałeczki na miejsce i ponownie zatapia się w rozmyślaniach.
Rys. Sytuacja posilających się filozofów.
Problem pięciu posilających się filozofów jest klasycznym problemem synchronizacji, z uwagi na to, że stanowi przykład szerokiej klasy problemów sterowania współbieżnością. Jest dobrym odzwierciedleniem konieczności przydzielania wielu zasobów do wielu procesów w sposób grożący zakleszczeniem i głodzeniem. Zakleszczenie - sytuacja, w której kilka procesów czeka w nieskończoność na zdarzenie, które może być spowodowane tylko przez jeden z czekających procesów.
W jednym z prostych rozwiązań przyjmuje się, że pałeczka jest semaforem. Filozof próbuje wziąć pałeczkę, wykonując operację czekaj odnoszącą się do danego semafora, a odkłada za pomocą operacji sygnalizuj. Zatem dane dzielone przedstawiają się w następujący sposób :
var pałeczka : array[0..4] of semaphore;
przy czym wszystkie elementy tablicy pałeczka mają na początku wartość 1.
Struktura i - tego filozofa :
repeat
czekaj (pałeczka[i]);
czekaj(pałeczka[i + 1 mod 5]);
...
jedzenie
...
sygnalizuj(pałeczka[i]);
sygnalizuj(pałeczka[i + 1 mod 5]);
...
myślenie
...
until false;
Mimo iż to rozwiązanie zapewnia, że żadni dwaj filozofowie nie będą jedli jednocześnie, musi być odrzucone, ponieważ kryje w sobie możliwość powstania zakleszczenia. Gdy cała piątka filozofów zasiadła by równocześnie do stołu i każdy by podniósł pałeczkę, leżącą po jego lewej stronie, to wszystkie elementy pałeczka stały by się równe zero. Usiłowanie któregokolwiek z filozofów poniesienia pałeczki kończyło by się zawsze nie powodzeniem.
Sposoby rozwiązania problemu zakleszczenia :
Pozwolić zasiadać do stołu co najwyżej czterem filozofom naraz.
Zezwolenie na podniesienie pałeczki tylko wtedy gdy dwie są dostępne.
Rozwiązanie asymetryczne, tzn. filozof o numerze nieparzystym podnosi najpierw pałeczkę po lewej stronie, a potem sięga po pałeczkę po prawej. Natomiast filozof o numerze parzystym najpierw sięga po pałeczkę z prawej strony a następnie z lewej.
Każde z satysfakcjonujących rozwiązań nie może pozwolić by któryś z filozofów został zagłodzony na śmierć.
KOMUNIKATY
Windows API jest pełne funkcji sterujących mechanizmem okienek i wykonującym wszystkie zadania związane z systemem Windows. W C++ Builder funkcje API są umieszczane w komponentach. W czasie pracy z programem określa się, jakie zadania mają wykonywać komponenty, a one same przekazują Windows szczegóły określające, co jest do zrobienia. Jednak komponenty (VCL) nie są w stanie objąć każdej z tysięcy funkcji API i komunikatów, dlatego czasami trzeba sięgać wprost do samego źródła
→ Programy w Windows sterowane są zdarzeniami:
Rozmaite zbiory zdarzeń, takie jak: kliknięcie myszą, naciśnięcie klawisza, manipulowanie oknami, itp. generują komunikaty wysyłane do odpowiadających im okien (interfejsów aplikacji).
Aplikacje oczekują na komunikaty, odpowiednio na nie reagują, a następnie wracają do stanu oczekiwania na kolejne komunikaty.
Każda aplikacja również może wysyłać dowolne „swoje” komunikaty za pomocą funkcji SendMessage(...) .
→ Dla programów wykorzystujących GUI (Graphical User Interface) systemu Windows stworzono interfejs programowania aplikacji API (Application Programming Interface). Zawiera on kilkaset funkcji udostępnianych przez Windows (np. MessageBeep, MessageBox, itd.).
→ Po uruchomienu aplikacji w środowisku Windows najpierw wywoływana jest funkcja WinMain, która otrzymuje cztery parametry:
//Program „0” → przykład bardzo prostego programu wykorzystującego API
#include "windows.h"
int PASCAL WinMain( HINSTANCE identyfikator_aplikacji,
HINSTANCE identyfikator_poprzedniej_aplikacji,
LPSTR adres_tekstu_parametrow,
int poczatkowy_stan_okna )
{
MessageBeep( −1 ); // uruchamia generowanie standardowego dźwięku
MessageBox( NULL, // identyfikator okna programu
"Udało się. Nacisnij klawisz OK !!!", // wyświetlany tekst
"Moj pierwszy program", // tytuł okienka
MB_OK ); // styl okna komunikatu
// MB_OK. → Message Box zawierający tekst i klawisz OK
return 0 ;
}
Ogólna struktura funkcji WinMain jest zazwyczaj podobna:
Rejestracja wszystkich klas okien i przygotowanie innych zasobów wykorzystywanych przez aplikację (jeżeli nie było wcześniejszych kopii).
Nowe klasy okien rejestruje się poprzez podanie ich opisu (za pomocą struktury typu WNDCLASS zdefiniowanej w <windows.h>) oraz wywołanie funkcji RegisterClass(...).
Utworzenie okna lub okien, które mają się pojawić na początku wykonywania programu. Utworzenie okna nastepuje w wyniku wywołania funkcji CreateWindow(...) oraz wyświetlenie go na ekranie poprzez wywołanie funkcji ShowWindow(...).
Oczekiwanie na komunikaty i ich rozsyłanie do odpowiednich okien. Charakterystycznym fragmentem programów w Windows jest pętla oczekiwania na komunikaty (ang. message loop)
• • •
MSG komunikat;
while( GetMessage( &komunikat, NULL, 0, 0 ) )
{
TranslateMessage( &komunikat );
DispatchMessage( &komunikat );
}
• • •
Realizacja pozostałych zadań należy w programie do funkcji obsługujących poszczególne okna.
Tworzenie okien komunikatów o dowolnym kształcie.
W celu utworzenia okna komunikatu (znanego bardziej pod nazwą MessageBox) w dowolnym kształcie posłużymy się dwiema funkcjami API, a mianowicie funkcją - 'CreateMessageDialog(AnsiString Msg, TMsgDlgType DlgType, TMsgDlgButtons Buttons)' oraz funkcją - 'CreateRoundRectRgn (int X1, int Y1, int X2, int Y2, int X3, int Y3)' Przechodzimy do pliku źródłowego (np. Unit1.cpp) i umieszczamy na nim komponent 'Button1', następnie w zdarzeniu 'OnClick' umieszczamy całą procedurę. Przykład:
// Plik źródłowy np. Unit1.cpp
TForm1::Button4Click(TObject *Sender) |
W podanym przykładzie zostanie utworzone okno w kształcie prostokąta z zaokrąglonymi rogami. W powyższym przykładzie okno zostało zdefiniowane jako typ mtCustom, lecz dostępne są również inne typy:
mtWarning - ostrzeżenie,
mtErroe - błąd,
mtInformation - informacja,
mtConfirmation - pytanie,
mtCustom - bez stylu.
Jeśli natomiast chodzi o przyciski, to dostępne są następujące typy:
mbYes - tak,
mbNo - nie,
mbOK - OK,
mbCancel - anuluj,
mbAbort - przerwij.
mbRetry - gotowe,
mbIgnore - ignoruj,
mbAll - wszystkie,
mbNoToAll - nie na wszystkie,
mbYesToAll - tak na wszystkie,
mbHelp - pomoc,
Jest jedno, „ale", a mianowicie napisy na przyciskach będą w angielskiej wersji językowej.
Obsługa komunikatów (MessageBox).
Pisząc o komunikatach mam na myśli takie okienka dialogowe, które wyskakują, gdy program zwraca jakiś wyjątek lub wyświetla komunikaty ostrzegawcze lub informujące o jakimś zdarzeniu.Do wywoływania komunikatów służy funkcja ShowMessage i jest to najprostsza lecz nie jedyna metoda tworzenia komunikatu, a tak to wygląda:
// Plik źródłowy np. Unit2.cpp
TForm1::Button1Click(TObject *Sender) |
Jest jeszcze jedna funkcja umożliwiająca tworzenie komunikatu jest to MessageBox. Funkcja ta jest bardziej rozbudowana od ShowMessage ponieważ oprócz podania treści komunikatu umożliwia również podanie nazwy komunikatu wyświetlanej jako właściwość Caption okna komunikatu, możliwe jest również zdefiniowanie ikony która będzie wyświetlana z lewej strony treści komunikatu, no i można też zdefiniować przycisk lub kilka przycisków, które zostaną wyświetlone w oknie komunikatu.
Najprostszy sposób wywołania tej funkcji przedstawia się następująco:
// Plik źródłowy np. Unit1.cpp |
Ciało funkcji wygląda następująco:
MessageBox(void * hWnd, const char * lpText, const char * lpCaption, unsigned int uType)
Jako pierwszy parametr - void *nWnd - podajemy uchwyt do okna z którego wywołujemy komunikat najlepiej jest podać parametr: Handle.
Parametr drugi - const char * lpText - to treść komunikatu, przy czym jest to parametr typu char więc jeśli wprowadzimy tam tekst to wszystko jest w porządku jeżeli jednak będzie to treść łączona np. tekst i zawartość zmiennej typu String to należy dokonać konwersji do typu char, np:
MessageBox(Handle, ("Treść komunikatu" + Edit1->Text).c_str(), "Etykieta", MB_OK | MB_ICONSTOP);
Kolejny parametr - const char * lpCaption - to treść etykiety Caption okna komunikatu, zasada działania taka sama jak w przypadku parametru drugiego.
Ostatni parametr - unsigned int uType - jest najbardziej złożony ponieważ tutaj właśnie definiuje się typ przycisku oraz typ ikony wyświetlanych w oknie komunikatu. Niżej przedstawiam dostępne typy.
Lista stylu przycisków:
MB_OK Przycisk "OK".
MB_YESNO Przyciski "Tak" i "Nie".
MB_YESNOCANCEL Przyciski "Tak", "Nie" i "Anuluj".
MB_RETRYCANCEL Przyciski "Powtórz" i "Anuluj".
MB_OKCANCEL Przyciski "OK" i "Anuluj".
MB_ABORTRETRYIGNORE Przyciski "Przerwij", "Powtórz" i "Ignoruj".
Lista typów ikon:
MB_ICONINFORMATION Małe "i" w kółku.
MB_ICONEXCLAMATION Znak wykrzyknika w kółku.
MB_ICONSTOP Znak stopu.
MB_ICONQUESTION Znak zapytania.
Oprócz podanych stylów przycisków i typu ikon można również stosować modyfikatory:
MB_SYSTEMMODAL - Działanie aplikacji zostaje zawieszone, dopóki użytkownik nie odpowie na żądanie okna komunikatu.
MB_APPLMODAL - Użytkownik musi odpowiedzieć na zapytanie okna komunikatu, zanim będzie mógł kontynuować pracę w bieżącym oknie. Może się jednak przenieść do innych okien aplikacji i w nich pracować.
MB_DEFBUTTON1 - Pierwszy przycisk w oknie komunikatu jest przyciskiem domyślnym. Zwróć uwagę na to, że zawsze jest on domyślny, jeśli nie zaznaczono inaczej.
MB_DEFBUTTON2 - Drugi przycisk jest przyciskiem domyślnym.
MB_DEFBUTTON3 - Trzeci przycisk jest przyciskiem domyślnym.
Jak widać oprócz standardowego przycisku "OK" możliwe jest również stosowanie przycisków "TAK", "NIE", "ANULUJ", "POWTÓRZ", "PRZERWIJ" i "IGNORUJ", a to z kolei oznacza, że można odpowiadać na komunikaty i w zależności od wybranej odpowiedzi podjąć odpowiednie działanie.
Załóżmy, że tworzymy komunikat, który pyta użytkownika programu czy ma zapisać zmiany w jakimś tam pliku i w zależności od odpowiedzi, albo je zapisuje, albo nie:
// Plik źródłowy np. Unit1.cpp
if(iQuestion == ID_YES) |
Jak więc widać to na przykładzie zadeklarowano zmienną typu int i przypisano jej wartość zwracaną przez okno komunikatu, a następnie w zależności od tego, który przycisk został wybrany wykonywane są odpowiednie instrukcje. Oczywiście oprócz wartości ID_YES można stosować w sposób analogiczny pozostałe wartości, czyli: ID_NO, ID_CANCEL, ID_RETRY, ID_ABORT i ID_IGNORE.
Zamiast funkcji MessageBox najczęściej stosuje się funkcję złożoną, tworzoną dla całej aplikacji, a mianowicie:
Application->MessageBox(const char * lpText, const char * lpCaption, unsigned int uType)
Różnica pomiędzy tymi funkcjami polega tylko na tym, że
w przypadku Application->MessageBox nie stosuje się uchwytu okna, ponieważ funkcja jest przyporządkowana całej aplikacji a nie wybranemu formularzowi. Poza tym zasada używania tej funkcji jest identyczna jak w przypadku MessageBox.