API to Application Programming Interfejs, jest to zbiór funkcji systemu operacyjnego Windows. Dzięki API programista może odwoływać się bezpośrednio do systemu operacyjnego - wywoływać jego funkcje, funkcje API. Jest to jedyny machanizm bezpośredniego odwoływania się do systemu operacyjnego. Właściwie, każda aplikacja dla windows korzysta a API. API pozwala na odwoływanie się do wszystkich zasobów systemu operacyjnego. Można się dostać do wszystkiego, co system operacyjny może zaoferować, a więc: operacji na rejestrze systemowych, interfejsy multimedialne, sieciowe, komponenty interfejsu urzytkownika, baz danych i wiele innych. Istotą tego interfejsu jest niezależność od języka programowania, można wywoływać funkcje API w programach napisanych w C, C++, Basic'u, ObjectPascal'u, a nawet asemblerze. Jest to możliwe dzięki wprowadzeniu standardowego mechanizmu wywoływania funkcji STDCALL, będącego PASCAL'owską konwencją wywołań. Jako, że nic w systemie windows (mowa o aplikacjach) nie dziej się bez użycia funkcji API, są one wykożystywane do przesyłania i odbierania komunikatów, oraz dają dostęp do mechanizmów programowania współbieżnego, oferując operacje na semaforach i tworzenie wątków. W następnych rozdziałach zostanią zaprezentowane funkcje Win32 API stosowane do komunikacji międzyprocesorowej, operacjach na semaforach i wątkach.
Komunikaty
Komunikaty, a raczej mechanizm komunikatów, to system wymiany informacji pomiędzy procesami w systemie windows. Zdefinoiwanych jest wiele dziesiątek komunikatów, są to standardowe komunikaty systemowe.
Oprócz nich, każdy programista, może definiować swoje komunikaty tzw. komunikaty użytkownika. System windows i oprogramowanie dla tego systemu to aplikacje sterowane zdarzeniami. Gdzy nastąpi jakieś zdarzenie np. nacięnięcie klawisza klawiatury, poruszenie myszką, kliknięcie myszką, przesunięcie okna, zmiana rozmiaru okna, potrzeba odświerzenia okna i wiele innych, system przesyła do aktywnego okna lub okna które zostało aktywowane właściwy komunikat informujący o zdarzeniu. Informacja o zdarzeniu przekazywana jest wprost w komunikacie. Eventualne większe porcje danyc nie są przesyłane razem z komunikatem, przesyłany jest tylko wkaźnik do miejsca ich przechowywania.
Każdy komunikat na swój unikalny typu całkowitego bez znaku. Poniżej dla przykładu prezentacja komunikatów służących do informowaniu o zdarzeniach klawiatury:
WM_CHAR
WM_DEADCHAR
WM_GETHOTKEY
WM_HOTKEY
WM_KEYDOWN
WM_KEYUP
WM_KILLFOCUS
WM_SETFOCUS
WM_SETHOTKEY
WM_SYSCHAR
WM_SYSDEADCHAR
WM_SYSKEYDOWN
WM_SYSKEYUP
znaczenie każdego z nich nie będzie wyjaśnione, gdyż nie wiąże się to z tematem. Zaczniemy od omówienia funkcji API, które służą do wysyłania komunikatów, a następnie zostanie opisany mechanizm ich przetwarzania.
BOOL PostMessage(
HWND hWnd, |
// uchwyt okna docelowego |
UINT Msg, |
// identyfikator komunikatu |
WPARAM wParam, |
// pierwszy parametr komunikatu |
LPARAM lParam |
// drugi parametr komunikatu |
); |
|
Funkcja wysyła komunikat umieszczając go w kolejce komunikatów, nie czekając naprzetworzenie komunikatu.
VOID PostQuitMessage(
int nExitCode |
// kod zamknięcia |
); |
|
Funkcja wysyła komunikat WM_QUIT, najczęściej w odpowiedzi na komunikat WM_DESTROY,orgumentem jest kod zamknięcia okna.
BOOL PostThreadMessage(
DWORD idThread, |
// uchwyt do wątka |
UINT Msg, |
// identyfikator komunikatu |
WPARAM wParam, |
// pierwszy parametr komunikatu |
LPARAM lParam |
// drugi parametr komunikatu |
); |
|
Funkcja umieszcza komunikat w kolejce komunikatów danego wątka, i natychmiastowo kończy działanie.
LRESULT SendMessage(
HWND hWnd, |
// uchwyt okna docelowego |
UINT Msg, |
// identyfikator komunikatu |
WPARAM wParam, |
// pierwszy parametr komunikatu |
LPARAM lParam |
// drugi parametr komunikatu |
); |
|
Funkcja wysyła komunikat do okna lub wielu okien,wywołując funkcje obsługi danego komunikatu, funkcja kończy działaniedopiero po obsłużeniu komuniatu przez docelowe okienko.
Pozostałe funkcje: SendMessageCallback, SendMessageTimeout, SendNotifyMessage, nie zostaną omówione, gdyż w praktyce programistycznej nie często się z nich korzysta.
W opisie funkcji mówimy o przesyłaniu, lub za chwilę o odbieraniu komunikatów,i jako adresata wymienia się okno, a nie aplikację, dzieje się tak dlatego, że wkomunikaty przesyłane są w reakcji na zdarzenie, a zdarzenie nie dotyczy tylkosamej aplikacji ale najczęściej jakiegoś komponentu tej aplikacji.W systemie windows komponęty takie jak przyciski, listy, listy rozwijane,pola edycyjne itd. określane są także jako okienka, i potrafią samodzielnie obsługiwać komunikaty.
Obsługa komunikatów.
Do odbierania komunikatów służy funkcja:
BOOL GetMessage(
LPMSG lpMsg, |
// adres do struktury opisującej komunikat |
HWND hWnd, |
// uchwyt okna |
UINT wMsgFilterMin, |
// pierwszy komunikat |
UINT wMsgFilterMax |
// ostatni komunikat |
); |
|
Funkcja pobiera komunikat z kolejki i umieszcza go w strukturze:
typedef struct tagMSG { |
Dopóki funkcja odbiera komunikaty różne od WM_QUIT, zwraca wartość większą od zera, w przeciwnym razie 0.
Implementacja obsługi komuniatów. Wiemy, że funkcja GetMessage zwraca wartość dodatnią dopóki okno nie otrzyma komunikatu nakazującego zamknięcie, zatem do odbioru stosuje się pętlę:
// pętla obsługi komunikatów |
Dodatkowo jeśli odbierane są zdarzenia z klawiatury, w celu zamienienia symboli znaków na zanki stosuje się funkcję:
TranslateMessage(CONST MSG *lpMsg),
gdzie argumentem jest wskaźnik do struktury komunikatu, a nastepnie odesłać komunikat funkcją:
DispatchMessage(CONST MSG *lpMsg).
Dodatkowo okienko implementuje funkcję realizującą obsługę komunikatów (jeśli nie robi tego w pętli odbioru komunikatów co nie jest zalecane), reagowanie na wyszczególnione komunikaty z pomocą instrukcji switch.
Tak może wyglądać funkcja obsługi komunikatów. Oczywiście nazwa może być inna, tak jak obsługiwane komunikaty, zaleca się jednak zachowanie zgodności nazw.
Semafory
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ść.
API udostępnia 5 sposobów synchronizacji wątków. Są to:
o zdarzenia
o mutexy
o semafory
o sekcje krytyczne
o zegary oczekujące
Mechanizm sekcji krytycznej możliwy jest do wykorzystania tylko w obrębie jednego procesu (do synchronizacji wątków), jednak jest to metoda najszybsza i najwydajniejsza. Pozostałe metody mogą być stosowane również dla wielu procesów
Zdarzenia - API umożliwia definiowanie własnych zdarzeń za pomocą funkcji CreateEvent( ). Zdarzenie może być zgłoszone i obowiązuje w systemie dopóty nie nastąpi jego odwołanie. Każdy oczekujący wątek widzi więc zdarzenie jako pewną dwustanową flagę: zdarzenie jest zgłoszone albo odwołane. Za pomocą funkcji SetEvent( ) informujemy system o zaistnieniu zdarzenia. Od tej pory zdarzenie jest zgłoszone i wszystkie wątki oczekujące do tej pory na jego zgłoszenie mogą wznowić działanie. Zdarzenie zostaje odwołane, kiedy zostanie wywołana funkcja ResetEvent( ). Na zaistnienie wydarzenia w systemie wątki oczekują za pomocą funkcji WaitForSingleObject( ).
Mutex - nazwa mutex pochodzi od angielskiego terminu mutual exclusion (wzajemne wykluczanie). Mutex jest obiektem służącym do synchronizacji. Jego stan jest ustawiony jako "sygnalizowany", kiedy żaden wątek nie sprawuje nad nim kontroli oraz "niesygnalizowany" kiedy jakiś wątek sprawuje nad nim kontrolę. Synchronizację za pomocą mutexów realizuje się tak, że 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.
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.
Sekacja 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:
o Wzajemne wykluczanie : jeśli jakiś proces, działa w swojej sekcji krytycznej, to żaden inny proces nie działa w sekcji krytycznej.
o 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ść.
o 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.
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 :
. 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 :
HANDLE CreateSemaphore
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, |
// właściwości bespieczeństwa |
LONG lInitialCount, |
// wartość początkowa |
LONG lMaximumCount, |
// wartość maksymalna |
LPCTSTR lpName |
// wskaźnik do nazwy semafora |
); Funkcja służy do stworzenia obiektu semafora. |
|
BOOL ReleaseSemaphore(
HANDLE hSemaphore, |
// uchwyt do semafora |
LONG lReleaseCount, |
// wartość do dodania do aktualnej wartości |
LPLONG lpPreviousCount |
// wskaźnik do poprzedniej wartości |
); Funkcja realizuje akcję "sugnalizuj". |
|
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, |
// flagi dopstępu |
BOOL bInheritHandle, |
// czy uchwyt jest dziedziczony |
LPCTSTR lpName |
// wskaźnik do nazwy semafora |
); Funkcja realizuje akcję "czekaj". |
|
DWORD WaitForSingleObject(
HANDLE hHandle, |
// uchwyt do obiektu |
|
|
DWORD dwMilliseconds |
// maksymalny czas oczekiwania |
); |
|
Funkcja umożliwa zatrzymanie wykonywania do momentu gdy obiekt nie zasybnalizuje smiany stanu.
DWORD WaitForMultipleObjects(
DWORD nCount, |
// ilość obiektów |
CONST HANDLE *lpHandles, |
// tablica obiektów |
BOOL bWaitAll, |
// wyjście gdy wszystkie obiekty asygnalizują zmianę stanu lub gdy tylko jeden z nich |
DWORD dwMilliseconds |
// maksymalny czas oczekiwania |
); Działanie tak jak powyżej, tylko dla grupy obiektów, parametr bWaitAll jeśli TRUE funkcja kończy działanie gdy wszystkie obiekty zmienią stan, gdy FALSE gdy jeden z nich |
|
//procesA
Handle a_semaphore = CreateSemaphore(NULL, l, l, "MySEMl");
// proces B
Handle b_semaphore = OpenSemaphore(SEMAPHORE_ALL_ACCESS.
FALSE, "MySEMl"); ...
Kod umożliwiający dzielenie obiektu za pomocą odnalezionej nazwy
// Proces A chce udzielić procesowi B dostępu do semafora.
// proces A
Handle a_semaphore = CreateSemaphore(NULL, l, l, NULL);
// wysłanie wartości semafora do procesu B za pomocą komunikatu
// lub wspólnej pamięci
// proces B
Handle process_a = OpenProcess(PROCESS_ALL_ACCESS, FALSE.
process_id_of_A);
Handle b_semaphore;
DuplicateHandle(process_a, a_semaphore,
GetCurrentProcess(),
&b_semaphore, O, FALSE,
DUPLICATE_SAME ACCESS):
II dostęp do semafora za pomocą uchwytu b_semaphore
Kod umożliwiający dzielenie obiektu za pomocą przekazanego uchwytu
Wątki
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 (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 API związane z wątkami :
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, |
// wskaźnik do struktury opisującej właściwości bespieczeństwa |
DWORD dwStackSize, |
// początkowy rozmiar stosu |
LPTHREAD_START_ROUTINE lpStartAddress, |
// wskaźnik do funkcji wątka |
LPVOID lpParameter, |
// argumenty |
|
|
DWORD dwCreationFlags, |
// flagi |
|
|
LPDWORD lpThreadId |
// wskaźnik w którym zwracany jest identyfikator wątka |
); |
|
DWORD ResumeThread(
HANDLE hThread |
// wskażnik do wątka, który ma być wznowiony |
); |
|
DWORD SuspendThread(
HANDLE hThread |
// wskażnik do wątka, który ma być zawieszony |
); |
|
VOID Sleep(
DWORD dwMilliseconds |
// czas uśpienia wątka |
); |
|
VOID ExitThread(
DWORD dwExitCode |
// kod zamknięcia |
); |
|
DWORD WaitForSingleObject(
HANDLE hHandle, |
// uchwyt do obiektu |
|
|
DWORD dwMilliseconds |
// maksymalny czas oczekiwania |
); |
|
DWORD WaitForMultipleObjects(
DWORD nCount, |
// ilość obiektów |
CONST HANDLE *lpHandles, |
// tablica obiektów |
BOOL bWaitAll, |
// wyjście gdy wszystkie obiekty asygnalizują zmianę stanu lub gdy tylko jeden z nich |
DWORD dwMilliseconds |
// maksymalny czas oczekiwania |
); |
|
funkcja dzięki której wątek oczekuje na przejście wielu obiektów w stan sygnalny.
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( ).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 :
BOOL SetThreadPriority(
HANDLE hThread, // uchwyt wątku
int nPriority // priorytet );
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).
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.
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
Dotarliśmy do końca niniejszego artykułu. Mam nadzieję, że przybliżyłem państwu aspekty programowania w Windows, związane z komunikatami oraz mechanizmami semaforów i wątków. Omawiane zagadnienia są szerokie, nieocenione staje się korzystanie z dokumentacji technicznej Win32 SDK, opisującej wszystkie aspekty programowania w Windows. Więcej informacji można zanleźć w MSDN'ie Microsoft'u. Jest to bazdzo rozległa baza wiedzy na temat wszystkich produktów firmy Microsoft.