Programowanie Aplikacji Sieciowych

Transkrypt

Programowanie Aplikacji Sieciowych
Politechnika
Białostocka
Wydział Elektryczny
Katedra Telekomunikacji i Aparatury Elektronicznej
Pracownia Specjalistyczna
Programowanie Aplikacji Sieciowych
Ćwiczenie 7
Programowanie wielowątkowe w Windows
opracował mgr inż. Grzegorz Kraszewski
Białystok 2007
Cel ćwiczenia
Celem ćwiczenia jest zapoznanie się ze sposobami tworzenia aplikacji wielowątkowych w systemie
Windows, ze szczególnym uwzględnieniem aplikacji sieciowych.
Materiały źródłowe
MicroSoft Developer Network:
Tworzenie wątków – http://msdn2.microsoft.com/en-us/library/ms682516.aspx
Muteksy – http://msdn2.microsoft.com/en-us/library/ms684266.aspx
Zdarzenia – http://msdn2.microsoft.com/en-us/library/ms682655.aspx
Sposób wykonania zadania
Bazując na wiadomościach z poprzednich zajęć, należy napisać prosty, wielowątkowy serwer HTTP,
zwracający stronę podającą ile razy została ona otwarta.
W systemach operacyjnych z rodziny Windows, wątki danego procesu to jak gdyby podprogramy
wykonywane przez procesor „jednocześnie”, co osiąga się przez podział czasu procesora między nie (w
przypadku maszyn wielordzeniowych czy wieloprocesorowych, wątki mogą być fizycznie wykonywane
jednocześnie przez różne rdzenie lub procesory). Każdy wątek posiada własny stos (dzięki temu każdy
ma własne kopie zmiennych lokalnych) i własny kod, natomiast wszystkie wątki jednego procesu
korzystają z tej samej przestrzeni adresowej i mają dostęp do tych samych zmiennych globalnych.
Uruchamianie wątków
Do uruchamiania nowych wątków w systemie Windows służy funkcja CreateThread(). Funkcja ta
wymaga 6 parametrów, które oznaczają kolejno:
1. Parametry bezpieczeństwa – wskaźnik do struktury opisującej czy identyfikator wątku może zostać
przekazany innym procesom. Typowo podajemy tu NULL, co oznacza, że identyfikator wątku
dostępny jest tylko dla procesu, który go stworzył.
2. Rozmiar stosu wątku – podanie wartości 0 spowoduje przydzielenie domyślnego rozmiaru stosu.
3. Adres funkcji – adres funkcji, która jest kodem tworzonego wątku.
4. Parametr – wskaźnik na dowolne dane, jakie można przekazać do wątku.
5. Flagi – najczęściej 0, można podać flagę CREATE_SUSPENDED, która powoduje, że wątek po
utworzeniu jest w stanie wstrzymania i można go „odpalić” później. Normalnie wątek startuje od
razu po stworzeniu.
6. Wskaźnik na zmienną, gdzie zostanie umieszczony identyfikator procesu.
Wynikiem funkcji jest wskaźnik na strukturę wątku, poprzez którą możemy go kontrolować. W
wypadku niepowodzenia funkcja zwraca NULL. A oto przykład stworzenia prostego wątku:
DWORD thread (void *p)
{
/* tu zadania wykonywane w wątku */
return 0;
}
int main(int argc, char *argv[])
{
HANDLE thr;
void *dowolne_dane = NULL;
if (thr = CreateThread(NULL, 0, thread, dowolne_dane, 0, NULL))
{
WaitForSingleObject(thr, INFINITE);
}
return 0;
}
thread() to funkcja, która zostanie wykonana jako wątek. Typem zwracanej wartości jest słowo 32bitowe. Parametr p to parametr przekazywany do wątku jako czwarty argument CreateThread(). Ważne
jest, aby ta zmienna (i dane przez nią wskazywane, jeżeli to wskaźnik) istniała przez cały czas pracy
wątku. Najprostszym sposobem jest zaczekanie w głównym wątku na zakończenie się wątku
potomnego. W przykładzie jest to zrobione przez wykonanie funkcji WaitForSingleObject() na
uchwycie wątku. W przypadku wielu wątków, należy uchwyty przechowywać w tablicy lub innej
strukturze danych, a do czekania użyć WaitForMultipleObjects().
Dostęp wielu wątków do tych samych danych
Jak wyżej wspomniano, obszar zmiennych globalnych, oraz tak zwanej „sterty” (heap) procesu, jest
wspólny dla wszystkich wątków. Może to spowodować problem jednoczesnego dostępu wątków do
danych. Problem powstaje kiedy jeden z wątków próbuje odczytać daną w momencie, kiedy jest
modyfikowana przez inny wątek. Dane (i inne zasoby) dostępne dla wielu wątków są bezpieczne tylko
w następujących przypadkach:
1. Dane są modyfikowane przed uruchomieniem wątków, a przez wątki są tylko czytane.
2. Dane są przez wątki tylko zapisywane.
3. Dane są czytane i modyfikowane wyłącznie przez jeden wątek.
Należy pamiętać o tym, że dla powyższych reguł główny proces (ten z którego uruchamiamy wątki),
jest również traktowany jako wątek.
Ponieważ często będziemy mieli do czynienia z sytuacją, gdy wątki czytają i zapisują dane, niezbędny
jest mechanizm, który pozwoli na sterowanie dostępem do danych z wątków. Windows wprowadza
wiele takich mechanizmów, tu omówimy najprostszy z nich, tak zwany mutex, który uniemożliwia
wątkom jednoczesny dostęp do chronionych danych. Do danych prawidłowo zabezpieczonych
mutexem, w danej chwili ma dostęp tylko jeden wątek.
Mutex tworzy się przed uruchamianiem wątków:
HANDLE moj_mutex;
moj_mutex = CreateMutex(NULL, 0, NULL);
Następnie każdy wątek (również i proces główny) przed dostępem do chronionych danych musi
wywołać funkcję WaitForSingleObject(). Funkcja ta sprawdza stan mutexa, jeżeli nie jest on zajęty
przez żaden inny wątek, to staje się on zajęty przez ten wątek, który wywołał funkcję. Powoduje to, że
pozostałe wątki, próbując wywołać tę funkcję, zostaną zatrzymane do czasu zwolnienia mutexa. W
efekcie, o ile wszystkie wątki przestrzegają zasady zajmowania mutexa przed dostępem do danych, w
danym momencie dostęp do nich ma tylko jeden z nich. Po zakończeniu dostępu do danych, wątek
powinien zwolnić mutexa, pozwalając na dostęp pozostałym wątkom. Robi się to przy pomocy funkcji
ReleaseMutex():
ReleaseMutex(moj_mutex);
Synchronizacja wątków
Ważnym zagadnieniem jest synchronizowanie wątków między sobą. Z koniecznością synchronizacji
mamy do czynienia, gdy jedne wątki procesu wykorzystują wyniki działania innych wątków. W
systemie Windows podstawowym mechanizmem synchronizacji międzywątkowej są zdarzenia
(events). Po stworzeniu zdarzenia, wątek może je ustawić w stan aktywny (signalled), w czasie gdy
inny wątek oczekuje na ten stan (używając na przykład WaitForSingleObject()). W ten sposób wątki
mogą wzajemnie kontrolować swoje wykonywanie się. Oto przykład:
HANDLE zdarzenie;
DWORD watek (void *p)
{
/* pierwszy etap zadań */
SetEvent(zdarzenie);
/* drugi etap zadań */
return 0;
}
int main(int argc, char *argv[])
{
HANDLE thr;
zdarzenie = CreateEvent(NULL, FALSE, FALSE, NULL);
if (thr = CreateThread(NULL, 0, watek, NULL, 0, NULL))
{
WaitForSingleObject(zdarzenie, INFINITE);
printf("Pierwsza część zadania wykonana.\n");
WaitForSingleObject(thr, INFINITE);
printf("Całe zadanie wykonane.\n");
}
return 0;
}

Podobne dokumenty