14. WĄTKI Procesy w tradycyjnym sensie (tworzone przez wykonanie funkcji fork) mają przydzielaną oddzielną przestrzeń adresową. W przestrzeni tej jest umieszczany oddzielny zestaw zmiennych środowiska, a w jądrze systemu - dane na temat procesu (atrybuty). Łączny koszt (narzut czasowy) związany z utworzeniem nowego procesu jest tak duży, że przeważnie nie opłaca się tworzyć procesów w celu przydzielenia im fragmentów obliczeń w programach transformacyjnych (chyba, że te fragmenty są odpowiednio duże, a my współdzielimy moc obliczeniową komputera z innymi użytkownikami lub dysponujemy maszyną wieloprocesorową). Tworzenie procesów jest raczej uzasadnione ze względu na logiczną organizację programów reaktywnych obsługujących wielu użytkowników (serwerów). Z wyżej wymienionych powodów procesy w tradycyjnym sensie są czasem nazywane procesami ciężkimi (heavyweight process).
W sytuacji, gdy zadanie zlecane przez jednego użytkownika może być podzielone na niezależne podzadania (klasyczny przykład - obliczenia macierzowe), celowe wydaje się uruchomienie wielu procesów we wspólnej przestrzeni adresowej (na przykład wspólnie przetwarzających oddzielne fragmenty dużej macierzy). Może to być zrealizowane przy wykorzystaniu pamięci dzielonej, ale jest to sposób dość kłopotliwy, a wiele informacji jest niepotrzebnie powielanej (na przykład dane właściciela, tablica deskryptorów plików...). W ten sposób powstała koncepcja wątków (thread), które w niektórych sytuacjach nazywane są też procesami lekkimi (lightweight process). Wątki okazały się narzędziem na tyle efektywnym (choć niełatwym w programowaniu), że nawet zaczęły wypierać procesy ciężkie z ich tradycyjnych dziedzin zastosowań (niektóre serwery współbieżne są realizowane przy użyciu wątków).
Wątki posiadają najbardziej istotne cechy procesów - dysponują własnymi zestawami rejestrów, a w szczególności wskaźnikami instrukcji (zatem mają przydzielone procesory - rzeczywiste lub wirtualne). Mają też własne stosy (więc mogą wywoływać funkcje). Wiele cech wątków zależy od konkretnej implementacji - w Linuxie zaimplementowana jest biblioteka wątków odpowiadająca normie POSIX (pthread), ale tylko w zakresie najważniejszych funkcji. program procesor IP dane procesor 2 IPdane procesor n IP procesor 1 IP proces jednowątkowy proces wielowątkowy
Jednym z podstawowych problemów, jakie muszą rozstrzygnąć implementatorzy, to czy wątki mają być reprezentowane w strukturach jądra systemu (jako procesy lekkie) i podlegać szeregowaniu globalnemu, czy też mają być niewidoczne dla jądra i podlegać szeregowaniu lokalnemu (w obrębie swojego procesu). W tym drugim przypadku programista może mieć wybór strategii szeregowania w obrębie poszczególnych procesów. Szeregowanie wiąże się z pojęciem priorytetu (wątki, tak jak procesy ciężkie, mają swoje indywidualne priorytety): czy priorytety wątków mają być traktowane jako bezwzględne współczynniki pilności wykonania, czy też mają być relatywizowane względem priorytetu ich procesu ? Kolejnym problemem do rozstrzygnięcia są skutki wywołań funkcji systemowych przez poszczególne wątki. Intencją projektantów jest uniezależnienie innych wątków w procesie od skutków wywołania funkcji przez jeden z nich. W szczególności wątek czasowo zawieszony z powodu oczekiwania na dane do wczytania nie powinien blokować pracy innych wątków tego samego procesu. Uproszczony diagram stanów wątku jest taki sam, jak uproszczony diagram stanów procesu: utworzonygotowy aktywny wstrzy many zakończony
Model logiczny wątków (z punktu widzenia programisty): - nowopowstały (wskutek wykonania fork) proces ciężki ma dokładnie jeden wątek; - każdy wątek ma prawo utworzyć pewną liczbę innych wątków w obrębie tego samego procesu (w granicach przyznanych zasobów systemowych); - wątek prywatnie posiada niewiele atrybutów i niewiele ich może przekazać w dziedzictwie (maska sygnałów, priorytet,...); - utworzone wątki są równoprawne (rodzeństwo) i nie przechowują informacji o swoim pokre- wieństwie (na przykład relacji przodek - potomek); - utworzony wątek może być w dwóch stanach (uwaga: to jest klasyfikacja niezależna od wyżej przedstawionych stanów wyróżnionych ze względu na szeregowanie wykonania): przyłączalny (joinable) lub odłączony (detached); wątek odłączony nie może się stać z powrotem przyłączalny; utworzony przyłączalny odłączony zakończony
- to, że wątek jest przyłączalny, oznacza w praktyce, że inny wątek (w tym samym procesie ciężkim) może zsynchronizować się z jego zakończeniem - wcielić (join) ten wątek; - wątki mogą reagować na sygnały indywidualnie (szczególnie, jeśli to one same spowodowały zaistnienie sytuacji wyjątkowej generującej sygnał); niektóre sygnały dotyczą jednak całych procesów wielowątkowych (w szczególności kill -9); - wcielenie jednego wątku przez drugi jest najbardziej elementarnym sposobem ich synchronizacji, ale istnieją też bardziej zaawansowane narzędzia - muteksy (mutex - mutual exclusion), będące bardzo uproszczoną realizacją semaforów binarnych (o odwrotnej interpretacji stanów: 0 - muteks otwarty; wartość niezerowa - muteks zamknięty). Muteksy zazwyczaj służą do synchronizacji wątków w obrębie jednego procesu, ale umieszczone w segmencie pamięci wspólnej mogą synchronizować wątki w różnych procesach. W rozszerzeniach normy POSIX (na razie rzadko implementowanych) zaprojektowanych jest kilka innych mechanizmów synchronizacji wątków - blokady zapisu/odczytu, semafory dla wątków, zmienne warunkowe i inne. Wszystkie one mają charakter obiektowy, to znaczy dostęp do nich jest umożliwiany przez rodziny odpowiednich funkcji bibliotecznych.
Atrybuty wątków są ustalane w momencie ich tworzenia przez wskazanie na obiekt atrybutów (zbiór atrybutów, często jeden z wielu, utworzony wcześniej odpowiednimi funkcjami). W trakcie działania wątku jego atrybuty można również zmieniać, ale nie poprzez zmianę obiektu atrybutów (to nie dałoby żadnego rezultatu), tylko poprzez zastosowanie innych funkcji, operujących bezpośrednio na wątku. Programy w języku C operujące na wątkach pod systemem Linux (i większością innych systemów unixowych) jako pierwszą dyrektywę muszą podawać #define _REENTRANT powodującą, że kod wynikowy programu będzie wielowchodliwy (wielokrotnego użytku). Włączany plik nagłówkowy:. Polecenie kompilacji: gcc nazwa.c -lpthread. Uwaga: jeśli program wielowątkowy używa również biblioteki Xlib, to przed pierwszym wywołaniem w programie funkcji XOpenDisplay( ) należy wywołać funkcję XInitThreads( ).
int pthread_create (pthread_t ident, pthread_attr_t atryb, void ( funkcja)(void ), void argument); Zwraca: 0 w przypadku sukcesu; niezerowy kod w przypadku błędu. ident - wskaźnik do zwracanego identyfikatora utworzonego wątku (liczby naturalnej unikalnej w obrębie danego procesu) atryb - wskaźnik do utworzonego wcześniej obiektu atrybutów (jeśli podamy NULL, będą przyjęte wartości domyślne atrybutów) funkcja - wskaźnik do adresu początkowego kodu funkcji, którą wątek ma wykonać argument - wskaźnik do ewentualnego argumentu tej funkcji (jeśli funkcja nie ma argumentu, podajemy NULL) Działanie: tworzy nowy wątek działający współbieżnie z wątkiem rodzicielskim i wykonujący wskazaną funkcję. Wątek kończy działanie w sposób naturalny, jeśli napotka funkcję pthread_exit, lub jeśli dojdzie do końca wykonywanej funkcji (wtedy pthread_exit jest wywoływana niejawnie).
void pthread_exit (void wartość); wartość - wskaźnik do wartości, którą wątek ma zwrócić Działanie: wykonuje ewentualne funkcje sprzątające (jeśli zostały zaprogramowane dla wątku), zwalnia zajmowane przez wątek zasoby, jeśli wątek został wcześniej odłączony, kończy działanie wątku. int pthread_join (pthread_t id, void wart); Zwraca: 0 w przypadku sukcesu; niezerowy kod w przypadku błędu. id - identyfikator wątku (uzyskany z pierwszego argumentu funkcji pthread_create) wart - wskaźnik do adresu w pamięci, pod którym ma być zapisana wartość zwracana przez wcielany wątek (podanie NULL oznacza rezygnację z informacji o zwracanej wartości)
Działanie: wstrzymuje wykonywanie wątku wywołującego tę funkcję aż do zakończenia wykonywania wątku o identyfikatorze id, jeśli: 1) wątek o identyfikatorze id istnieje i nie jest odłączony; 2) inny wątek nie czeka już na jego zakończenie; 3) nie jest to ten sam wątek, który wywołuje tę funkcję (wątek nie może czekać na własne zakończenie). Jeśli drugi argument nie jest równy NULL, pobiera informację o wartości zwróconej przez wątek o identyfikatorze id. Na koniec powoduje zwolnienie zasobów zajmowanych przez ten wątek. Uwaga. Gdyby wątek przyłączalny zakończył działanie, a żaden inny wątek nie wcieliłby go, zajmowane przez niego zasoby byłyby odzyskane przez system dopiero w chwili zakończenia całego procesu.
int pthread_detach (pthread_t id); Zwraca: 0 w przypadku sukcesu; niezerowy kod w przypadku błędu. id - identyfikator wątku Działanie: odłącza (detach) wątek o identyfikatorze id (w szczególności wątek może odłączyć sam siebie), jeśli: 1) wątek o tym identyfikatorze istnieje i jest przyłączalny; 2) inny wątek nie czeka już na jego zakończenie (nie wywołał funkcji pthread_join z argumentem id ). Uwaga. Wątek może zakończyć również swoje wykonywanie wskutek jego unieważnienia (cancellation) przez inny wątek.