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; }