Wątki

Wątki


Koncepcja wątku (ang. thread) wiąże się ze współdzieleniem zasobów. Każdy proces (ciężki proces w odróżnieniu od lekkiego, czyli wątku) otrzymuje zasoby od odpowiedniego zarządcy i utrzymuje je do swojej dyspozycji. Zasoby przydzielone procesowi wykorzystywane są na potrzeby sekwencyjnego wykonania programu, ale w wyniku wykonania programu mogą się pojawić kolejne żądania zasobowe. Niedostępność żądanego zasobu powoduje zablokowanie procesu (wejście w stan oczekiwania). W programie procesu może być jednak inny niezależny fragment, do wykonania którego żądany zasób nie jest potrzebny. Można by zatem zmienić kolejność instrukcji w programie i wykonać ten niezależny fragment wcześniej, jednak do jego wykonania może być potrzebny inny zasób. Dostępność zasobów zależy od stanu całości system i w czasie tworzenia programu nie wiadomo, które z nich będą dostępne, gdy będą potrzebne do wykonania jednego czy drugiego fragmentu.

W programie dobrze jest zatem wówczas wyodrębnić takie niezależne fragmenty i wskazać systemowi, że można je wykonać w dowolnej kolejności lub nawet współbieżnie, w miarę dostępnych zasobów. Taki wyodrębniony fragment określa się jako wątek. Wątek korzysta głównie z zasobów przydzielonych procesowi — współdzieli je z innymi wątkami tego procesu. Zasobem, o który wątek rywalizuje z innymi wątkami, jest procesor, co wynika z faktu, że jest on odpowiedzialny za wykonanie fragmentu programu. Wątek ma więc własne sterowanie, w związku z czym kontekst każdego wątku obejmuje licznik rozkazów, stan rejestrów procesora oraz stos. Każdy wątek musi mieć swój własny stos, gdzie odkładane są adresy powrotów z podprogramów oraz alokowane lokalne zmienne.

Realizacja wątków


Tworzenie dodatkowych wątków w ramach tego samego procesu oraz przełączanie kontekstu pomiędzy nimi jest ogólnie mniej kosztowne, niż w przypadku ciężkich procesów, gdyż wymaga przydziału lub odpowiedniej zmiany stanu znacznie mniejszej liczby zasobów. Wynika to z faktu, że wątki tego samego procesu współdzielą większość przestrzeni adresowej (segment danych, segment kodu), otwarte pliki i sygnały. Nie jest zatem konieczne wykonywanie dodatkowych działań związanych z zarządzaniem pamięcią. Wątki mogą być nawet tak zorganizowane, że jądro nie jest świadome ich istnienia. Deskryptory wątków utrzymywane są w pamięci procesu (a nie jądra) i cała obsługa wykonywana jest w trybie użytkownika.

Alternatywą jest zarządzanie wątkami w trybie systemowym przez jądro, które utrzymuje deskryptory i odpowiada za przełączanie kontekstu pomiędzy wątkami.

Istotne w obsłudze wielowątkowości, niezależnie od sposobu realizacji, jest dostarczenie odpowiednich mechanizmów synchronizacji wątków wewnątrz procesu. Potrzeba synchronizacji wynika z faktu współdzielenia większości zasobów procesu. Problemy synchronizacji będą przedmiotem rozważań w innym module.

Realizacja wątków na poziomie jądra systemu operacyjnego


Obsługa wielowątkowości na poziomie jądra systemu (w trybie systemowym) oznacza, że wszelkie odwołania do mechanizmów obsługi wątków wymagają dostępu do usług jądra, co zwiększa koszt czasowy realizacji. Jądro musi też utrzymać bloki kontrolne (deskryptory) wątków, co w przypadku wykorzystania statycznych tablic może stanowić istotny koszt pamięciowy.

Z drugiej strony, świadomość istnienia wątków procesu umożliwia uwzględnienie tego faktu w zarządzaniu zasobami przez jądro i prowadzi do poprawy ich wykorzystania.

Realizacja wątków w trybie użytkownika


Realizacja wątków przez odpowiednią bibliotekę w trybie użytkownika zwiększa szybkość przełączania kontekstu, ale powoduje, że jądro, nie wiedząc nic o wątkach, planuje przydział czasu procesora dla procesów. Oznacza to, że w przypadku większej liczby wątków procesu czas procesora, przypadający na jeden wątek jest mniejszy, niż w przypadku procesu z mniejszą liczbą wątków.

Problemem jest też wprowadzanie procesu w stan oczekiwania, gdy jeden z wątków zażąda operacji wejścia-wyjścia lub utknie na jakimś mechanizmie synchronizacji z innymi procesami. Planista traktuje taki proces jako oczekujący do czasu zakończenia operacji, podczas gdy inne wątki, o których jądro nie wie, mogłyby się wykonywać.

W niektórych systemach operacyjnych wyróżnia się zarówno wątki trybu użytkownika, jak i wątki trybu jądra. W systemie Solaris terminem wątek określa się wątek, istniejący w trybie użytkownika, a wątek trybu jądra określa się jako lekki proces. W systemie Windows wprowadza się pojecie włókna , zwanego też lekkim wątkiem (ang. fiber, lightweight thread), które odpowiada wątkowi trybu użytkownika, podczas gdy termin wątek odnosi się do wątku trybu jądra. Takie rozróżnienie umożliwia operowanie pewną liczbą wątków trybu jądra, a w ramach realizowanych przez te wątki programów może następować przełączanie pomiędzy różnymi wątkami trybu użytkownika bez wiedzy jądra systemu. Wątek trybu jądra można więc traktować jako wirtualny procesor dla wątku trybu użytkownika.

Przełączanie kontekstu wątków


slajd 25

Kontekst pomiędzy dwoma lekkimi procesami przełączany jest przez jądro. Każdy z lekkich procesów wykonuje jakiś wątek trybu użytkownika (włókno), co obrazuje ciągła linia ze strzałką. Dla każdego lekkiego procesu istnieje zatem bieżące włókno. W ramach wykonywanego kodu takiego włókna może nastąpić wywołanie funkcji zachowania bieżącego kontekstu, a następnie funkcji odtworzenia innego (wcześniej zachowanego) kontekstu, o ile tylko w miejscu wywołania dostępny jest odpowiedni deskryptor, opisujący odtwarzany kontekst. Potencjalnie więc każdy z lekkich procesów może wykonywać dowolne z włókien, co symbolizuje przerywana linia.

Obsługa wątków


  • Tworzenie wątku

    • POSIX: pthread_create
    • Windows: CreateThread, CreateRomoteThread
  • Usuwanie wątku

    • POSIX: pthread_exit, pthread_cancel
    • Windows: ExitThread, TerminateThread
  • Wstrzymanie i wznawianie wątku

    • POSIX: brak
    • Windows: SuspendThread, ResumeThread
  • Zmiana priorytetu wątku

    • POSIX: pthread_setschedprio, pthread_setschedparam
    • Windows: SetTreadPriority
  • Oczekiwanie na zakończenie wątku

    • POSIX: pthread_join
    • Windows: brak bezpośredniego wsparcia, należy użyć odpowiednich mechanizmów synchronizacji

Realizacja procesów/wątków w systemie Linux


Proces potomny tworzony jest w systemie Linux poprzez wywołanie funkcji clone. Funkcja ta wykorzystywana jest między innymi do implementacji funkcji fork, ujętej w standardzie POSIX.

Tworząc nowy proces z użyciem funkcji clone można określić, które zasoby procesu macierzystego mają być współdzielone z potomkiem. W zależności od zakresu współdzielonych zasobów, nowo utworzony proces może być uznawany za wątek lub za ciężki proces. Typowe wątki będą współdzielić przestrzeń adresową, otwarte pliki i inne informacje związane z systemem plików (np. katalog bieżący, korzeń drzewa katalogów) i procedury obsługi sygnałów.

Rozróżnienie proces ciężki – proces lekki sprowadza się zatem do określenia zakresu współdzielenia zasobów.

Stany procesu (wątku) w systemie Linux


  • TASK_RUNING — wykonywanie lub gotowość (do wykonania)
  • TASK_INTERRUPTIBLE — oczekiwanie na zajście zdarzenia lub sygnał
  • TASK_UNINTERRUPTIBLE — oczekiwanie na zajście zdarzenia, przy czym sygnały są ignorowane
  • TASK_ZOMBI — stan zakończenia utrzymywany w celu przechowania deskryptora procesu
  • TASK_STOP — zatrzymanie w wyniku otrzymania sygnału (np. SIGSTOP)

Cykl zmian stanów procesu (wątku) w systemie Linux


slajd 30

Cykl zmian stanów w Linuksie jest bardzo prosty — odpowiada dość dokładnie ogólnemu schematowi. Jedyna różnica to wyodrębnienie dwóch stanów oczekiwania — w jednym następuje reakcja na sygnały, w drugim sygnały są ignorowane. Jako specyficzny rodzaj oczekiwania można też traktować stan wstrzymania TASK_STOP. Specyficzną cechą jest również brak rozróżnienia pomiędzy stanem gotowości a stanem wykonywania. Są to oczywiście dwa różne stany, ale w deskryptorze procesu oznaczone w taki sam sposób.

Proces w systemie Windows 2000/XP


Proces w systemie Windows gromadzi zasoby na potrzeby wykonywania wątków, wchodzących w jego skład. Informacje o procesie znajdują się w strukturze EPROCESS, której częścią jest właściwy blok kontrolny (KPROCESS). Zawartość obu tych struktur dostępna jest w trybie jądra. W ich skład wodzi wiele wskaźników do innych struktur (między innymi struktur opisujących wątki). Część opisu procesu — blok środowiska procesu PEB — znajduje się w części przestrzeni adresowej, dostępnej w trybie użytkownika.

Wątki w systemie Windows 2000/XP


  • Wątki korzystają z zasobów przydzielonych procesom.
  • Wątki (nie procesy) ubiegają się o przydział procesora i są szeregowane przez planistę krótkoterminowego.
  • Struktury opisu wątku obejmują;
    • ETHREAD — blok centrum wykonawczego, opisujący wątek,
    • KTHREAD — blok kontrolny procesu, część struktury ETHREAD,
    • TED — blok środowiska procesu, dostępny w trybie użytkownika.

Podstawowe zasoby na potrzeby wykonania wątku (np. pamięć) przydzielone są procesowi. Są one zatem wspólne dla wszystkich wątków danego procesu. Najważniejszym zasobem przydzielanym wątkowi jest procesor. Wszelkie przetwarzanie i wynikająca stąd zmiana stanu procesu odbywa się w wątku. Struktury opisu wątku są analogiczne do struktur opisu procesu.

Stany wątku w systemie Windows 2000/XP


  • Inicjalizowany (initialized, wartość 0) — stan wewnętrzny w trakcie tworzenia wątku,
  • Gotowy (ready, wartość 1) — oczekuje na przydział procesu,
  • Wykonywany (running, wartość 2)
  • Czuwający (standby, wartość 3) — wybrany do wykonanie jako następny,
  • Zakończony (terminated, wartość 4),
  • Oczekujący (waiting, wartość 5) — oczekuje na zdarzenie,
  • Przejście (transition, wartość 6) — oczekuje na sprowadzenie swojego stosu jądra z pliku wymiany,
  • Unknown (wartość 7)

Cykl zmian stanów wątku w systemie Windows 2000/XP


slajd 34

W systemie Windows zarówno stan gotowości, jak i czuwania odpowiada stanowi gotowości w odniesieniu do ogólnego schematu zmian stanów.