8430


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:

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.

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 :

  1. 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 :

Funkcje związane z wątkami :

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 :

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 :

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 :

Zalety :

Wady :

Wady :

***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:

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.

  1. 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 :

Klasyczne definicje operacji czekaj i sygnalizuj są następujące :

czekaj (S) : while S0x01 graphic
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 ( S0x01 graphic
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 :

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.

  1. 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:

    1. 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.

    2. 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.

  1. 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.

0x01 graphic

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 :

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:

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:

  1. 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(...).

  2. 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(...).

  3. 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
//--------------------------------
void __fastcall

TForm1::Button4Click(TObject *Sender)
{
TForm*frm=newTForm(this);
frm=CreateMessageDialog("To jest przykładowy tekst komunikatu",mtCustom, TMsgDlgButtons() << mbOK);
HRGN rgn;
rgn = CreateRoundRectRgn(0, 0, frm->Width, frm->Height, 20, 20);
SetWindowRgn(frm->Handle, rgn, true);
DeleteObject(rgn);
frm->ShowModal();
}
//--------------------------------


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:

  1. mtWarning - ostrzeżenie,

  2. mtErroe - błąd,

  3. mtInformation - informacja,

  4. mtConfirmation - pytanie,

  5. mtCustom - bez stylu.

Jeśli natomiast chodzi o przyciski, to dostępne są następujące typy:

  1. mbYes - tak,

  2. mbNo - nie,

  3. mbOK - OK,

  4. mbCancel - anuluj,

  5. mbAbort - przerwij.

  6. mbRetry - gotowe,

  7. mbIgnore - ignoruj,

  8. mbAll - wszystkie,

  9. mbNoToAll - nie na wszystkie,

  10. mbYesToAll - tak na wszystkie,

  11. 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
//--------------------------------
void __fastcall

TForm1::Button1Click(TObject *Sender)
{
 ShowMessage("Treść komunikatu");
}
//--------------------------------

    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
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 MessageBox(Handle, "Treść komunikatu", "Etykieta", MB_OK | MB_ICONINFORMATION);
}
//--------------------------------

Ciało funkcji wygląda następująco:

MessageBox(void * hWnd, const char * lpText, const char * lpCaption,  unsigned int uType)

  1. Jako pierwszy parametr - void *nWnd - podajemy uchwyt do okna z którego wywołujemy komunikat najlepiej jest podać parametr: Handle.

  2. 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);

  3. Kolejny parametr - const char * lpCaption - to treść etykiety Caption okna komunikatu, zasada działania taka sama jak w przypadku parametru drugiego.

  4. 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:

Lista typów ikon:

Oprócz podanych stylów przycisków i typu ikon można również stosować modyfikatory:

    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
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 
int iQuestion = MessageBox(Handle, "Czy chcesz zapisać zmiany do pliku?", "Zapisywanie zmian", MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2);

 if(iQuestion == ID_YES)
 {
  // ...jeżeli TAK następuje wykonanie tej instrukcji...
 }
 
else
 {
  // ... jeżeli NIE następuje wykonanie tej instrukcji...
 }
}
//--------------------------------

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.



Wyszukiwarka