Procesy, wątki i wielozadaniowość

 

 

Proces - jedno z najbardziej podstawowych pojęć w informatyce, definiowane jako egzemplarz wykonywanego programu, jednak każdy nowo powstały proces otrzymuje unikalny numer, który go jednoznacznie identyfikuje, tzw. numer PID (ang. process IDentifier).

W celu wykonania programu system operacyjny przydziela procesowi zasoby (pamięć, czas procesora i inne - szczegółowa lista zasobów znajduje się dalej), ale także może być konieczne współbieżne wykonywanie pewnych fragmentów programu. Aby to zrealizować program może zażądać utworzenia określonej liczby wątków, wykonujących wskazane części programu - o ich współbieżne wykonanie dba system operacyjny (albo sam program, wówczas mówi się o zielonych wątkach). Wątki współdzielą prawie wszystkie zasoby zarezerwowane dla danego procesu, wyjątkiem jest czas procesora, który jest przydzielany indywidualnie każdemu wątkowi.

Za zarządzanie procesami odpowiada jądro systemu operacyjnego, sposób ich obsługi jest różny dla różnych systemów operacyjnych. W systemie operacyjnym każdy proces posiada proces nadrzędny, z kolei każdy proces może, poprzez wywołanie funkcji systemu operacyjnego, utworzyć swoje procesy potomne; w ten sposób tworzy się swego rodzaju drzewo procesów. Każdy proces otrzymuje od systemu operacyjnego odrębne zasoby, w tym odrębną przestrzeń adresową, listę otwartych plików, urządzeń itp.

W skład procesu wchodzi:

Każdemu procesowi przydzielone zostają zasoby, takie jak:

 

Użytkownik za pomocą powłoki zleca uruchomienie programu, proces wywołujący wykonuje polecenie fork, lub jego pochodną.

Dany proces rozpoczyna wykonywanie w momencie przełączenia przez Jądro systemu operacyjnego przestrzeni adresowej na przestrzeń adresową danego procesu oraz takie zaprogramowanie procesora, by wykonywał kod procesu. Wykonujący się proces może żądać pewnych zasobów, np. większej ilości pamięci. Zlecenia takie są na bieżąco realizowane przez system operacyjny.

Wykonanie procesu musi przebiegać sekwencyjnie. Może przyjmować kilka stanów:

 

Proces wykonuje ostatnią instrukcję - zwraca do systemu operacyjnego kod zakończenia. Jeśli proces zakończył się poprawnie zwraca wartość 0, w przeciwnym wypadku zwraca wartość kodu błędu.

 

Wątek (ang. thread) - część programu wykonywana współbieżnie w obrębie jednego procesu; w jednym procesie może istnieć wiele wątków.

Różnica między zwykłym procesem a wątkiem polega na współdzieleniu przez wszystkie wątki działające w danym procesie przestrzeni adresowej oraz wszystkich innych struktur systemowych (np. listy otwartych plików, gniazd, itp.) - z kolei procesy posiadają niezależne zasoby.

Ta cecha ma dwie ważne konsekwencje:

  1. Wątki wymagają mniej zasobów do działania i też mniejszy jest czas ich tworzenia.
  2. Dzięki współdzieleniu przestrzeni adresowej (pamięci) wątki jednego zadania mogą się między sobą komunikować w bardzo łatwy sposób, niewymagający pomocy ze strony systemu operacyjnego. Przekazanie dowolnie dużej ilości danych wymaga przesłania jedynie wskaźnika, zaś odczyt (a niekiedy zapis) danych o rozmiarze nie większym od słowa maszynowego nie wymaga synchronizacji (procesor gwarantuje atomowość takiej operacji).

Wątki są udostępniane wprost przez system operacyjny MS Windows, w systemach Linux, BSD i innych dostępna jest biblioteka pthread, dająca jednolity interfejs, ukrywający szczegóły implementacji. W językach programowania używających maszyn wirtualnych (Python, Java itp.) są dostępne również tzw. zielone wątki, które nie są obsługiwane przez system operacyjny, ale samą maszynę wirtualną - to pozwala m.in. na realizację współbieżności nawet wtedy, gdy docelowy system operacyjny nie udostępnia wątków.

W systemach wieloprocesorowych, a także w systemach z wywłaszczaniem, wątki mogą być wykonywane równocześnie (współbieżnie). Równoczesny dostęp do wspólnych danych grozi jednak utratą spójności danych i w konsekwencji błędem działania programu.

Podręcznikowy przykład: ciąg instrukcji odczyt-zmiana-zapis.
Załóżmy że program ma dane do przetwarzania, umieszczone w N pierwszych komórkach tablicy X. Liczba N zapisana jest w odpowiedniej zmiennej. Algorytm przetwarzania mógłby wyglądać następująco:

  1. odczytaj zmienną N i sprawdź, czy jest równa 0
  2. jeśli tak (nie ma danych w X), przejdź do kroku 7.
  3. (tu wchodzimy, gdy N równe 1 lub więcej) odczytaj wartość X[N]
  4. zmniejsz wartość N o 1 (zaznacz, że N-ta dana została już zabrana)
  5. zrób coś z tą odczytaną daną (tu następuje właściwe przetwarzanie)
  6. (dana obsłużona - zajmij się następną) przejdź do kroku 1.
  7. (koniec pracy)

Jest to najprostsza pętla opróżniająca stos X. W środowisku jednowątkowym działa zgodnie z oczekiwaniami, przetwarzając kolejno dane X[N], X[N-1], itd. aż do X[1], po czym zatrzymuje się z zerową wartością zmiennej N.

Jednak w środowisku wielowątkowym dwa równoczesne wątki mogą wykonać się w taki sposób (załóżmy N=2):

wątek 1

wątek 2

krok

czynność

krok

czynność

1.

odczyt N=2

 

 

2.

(nic; N > 0)

1.

odczyt N=2

3.

odczyt X[N], czyli X[2]

2.

(nic; N > 0)

 

 

3.

odczyt X[N], czyli X[2]

4.

zapis N=1

 

 

 

 

4.

zapis N=1

5.

(przetwarzanie)

5.

(przetwarzanie)

6.

(przejście do 1.)

6.

(przejście do 1.)

Jak widać, oba wątki pobrały do przetwarzania tę samą daną X[2]. Jeśli nasz program jest systemem księgowym, a w X[2] było zapisane "dokonaj przelewu kwoty xxxx z rachunku nnnn na rachunek mmmm", to przelew zostanie zaksięgowany dwukrotnie.

W dalszym ciągu wykonania tego samego programu możliwy jest również inny przypadek. Przypuśćmy, że wątek nr 1 wolniej przetwarzał X[2] i teraz wątek nr 2 zaczyna kolejny cykl:

wątek 1

wątek 2

krok

czynność

krok

czynność

 

 

1.

odczyt N=1

1.

odczyt N=1

2.

(nic; N > 0)

 

 

3.

odczyt X[N], czyli X[1]

2.

(nic; N > 0)

 

 

 

 

4.

zapis N=0

3.

odczyt X[N], czyli X[0]

 

 

 

....

 

....

Pomimo posłużenia się licznikiem N, wątek nr 1 usiłuje pobrać nieistniejący element danych spod nielegalnego indeksu - X[0]. W zależności od różnych czynników spowoduje to albo natychmiastowe awaryjne przerwanie działania programu, albo dowolne, nieprzewidywalne zaburzenia (błędy) w dalszym jego działaniu.

Do zapobiegania takim sytuacjom wykorzystuje się mechanizmy synchronizacji wątków: semafory, muteksy, sekcje krytyczne.

 

Wielowątkowość (ang. multithreading) – cecha systemu operacyjnego, dzięki której w ramach jednego procesu może wykonywać kilka wątków lub jednostek wykonawczych. Nowe wątki to kolejne ciągi instrukcji wykonywane oddzielnie. Wszystkie wątki tego samego procesu współdzielą kod programu i dane.

Wielowątkowość może także odnosić się do samych procesorów. W takim wypadku oznacza możliwość jednoczesnego wykonywania wielu wątków sprzętowych na pojedynczej jednostce wykonawczej – rdzeniu (ang. core). Wielowątkowość w procesorach możliwa jest dzięki temu że nie wszystkie części jednostki wykonawczej są w jednakowym stopniu wykorzystywane przez pojedynczy wątek (ciąg instrukcji). Nieaktywne części jednostki wykonawczej mogą w tym czasie wykonywać inny wątek zwiększając efektywność wykorzystania całego procesora. W zależności od rodzaju technik zastosowanych do obsługi dodatkowych wątków sprzętowych spotyka się od 2 (najczęściej) do nawet 8 wątków sprzętowych na pojedynczy rdzeń procesora (core).

Cechy wielowątkowości

Systemy wielowątkowe to m.in. BeOS, Microsoft Windows 95, Windows NT, Unix, Linux.

Wielozadaniowość – cecha systemu operacyjnego umożliwiająca mu równoczesne wykonywanie więcej niż jednego procesu. Zwykle za poprawną realizację wielozadaniowości odpowiedzialne jest jądro systemu operacyjnego.

Wielozadaniowość zapewniona jest między innymi przez planistę, czyli część systemu operacyjnego realizującą algorytm szeregowania zadań w kolejce do przyznania czasu procesora.

Równoczesność jest pozorna, gdy system ma dostępnych mniej procesorów niż zadań do wykonania. Wówczas dla uzyskania wrażenia wykonywania wielu zadań jednocześnie, konieczne staje się dzielenie czasu.

Systemy wielozadaniowe można podzielić na oferujące i nie oferujące wywłaszczania. W systemach z wywłaszczaniem może nastąpić przerwanie wykonywania procesu, odebranie mu procesora i przekazanie sterowania do planisty. Pełne wywłaszczanie zapewniają mechanizmy sprzętowe działające niezależnie od oprogramowania (np. dołączanie wywłaszczania do procedury obsługi przerwania zegarowego). W systemach bez wywłaszczania procesy powinny same dbać o sprawiedliwy podział czasu, co często uzyskuje się pośrednio - proces dokonując wywołania systemowego, oddaje sterowanie procesowi jądra, lub jednemu z procesów systemowych i w ten sposób zrzeka się procesora. Program nie wykonywany pozostaje "w uśpieniu" do momentu, gdy znów zostanie mu przydzielony czas procesora.

Dzielenie czasu, zwane podziałem czasu, w wielozadaniowych systemach operacyjnych, jest mechanizmem pozwalającym na wykonywanie jednocześnie wielu zadań (zwykle dotyczy procesów, czasem również wątków) w obrębie jednego procesora w pewnej, umownej, jednostce czasu.

Za mechanizm dzielenia czasu odpowiedzialny jest planista. Decyduje on o kolejności przełączania zadań oraz o wyznaczaniu okresu, na jaki danemu zadaniu przydziela się procesor. Takie postępowanie może sprawiać wrażenie równoległego wykonywania tychże zadań (przy czym każde z nich ma do dyspozycji tylko część, uzależnionej od planisty, mocy obliczeniowej procesora).

Aby dzielenie czasu funkcjonowało właściwie, konieczne jest zapewnienie możliwości wywłaszczania zadań.

W przypadku wieloprocesorowych systemów komputerowych, zadania mają do dyspozycji więcej niż jeden procesor i w takim wypadku mogą rzeczywiście wykonywać się jednocześnie.

Wywłaszczenie - to technika używana w środowiskach wielowątkowych, w której algorytm szeregujący (scheduler) może wstrzymać aktualnie wykonywane zadanie (np. proces lub wątek), aby umożliwić działanie innemu. Dzięki temu rozwiązaniu zawieszenie jednego procesu nie powoduje blokady całego systemu operacyjnego. W systemach bez wywłaszczenia zadania jawnie informują scheduler, w którym momencie chcą umożliwić przejście do innych zadań. Jeżeli nie zrobią tego w odpowiednim czasie, system zaczyna działać bardzo wolno. Oprócz tego wywłaszczanie umożliwia szczegółowe określanie czasu, w jakim dany proces może korzystać z procesora. Wywłaszczanie w niektórych systemach operacyjnych może dotyczyć nie tylko programów, ale także samego jądra - przykładem takiego systemu jest Linux.

Wywłaszczanie jest często ograniczane, na przykład procedury odpowiedzialne za obsługi przerwań sprzętowych są zwykle niewywłaszczalne, co znacznie upraszcza ich konstrukcje ale wymusza też zadbanie o to, żeby szybko się kończyły umożliwiając działanie innym procesom. W systemie Linux przed wersją 2.6 niemożliwe było wywłaszczenie procesu który znajdował się w trybie jądra, co w pewnych sytuacjach mogło być powodem bardzo wolnej reakcji na działania użytkownika.