Wykład 6 Programowanie systemowe w Linux: Wątki i ich synchronizacja UNIX jesień/zima 2013 Wykład 6 Programowanie systemowe w Linux: Wątki i ich synchronizacja dr inż. Wojciech Bieniecki Instytut Nauk Ekonomicznych i Informatyki http://wbieniec.kis.p.lodz.pl/pwsz
Wielowątkowość – powtórzenie Jeden proces może wykonywać się w wielu współbieżnych wątkach (ang. thread). Każdy wątek (inna nazwa: proces lekki, ang. lightweight process) – Ma swój własny stan (Aktywny, Gotowy, Zablokowany, ... ) – Ma swoje wartości rejestrów i licznika rozkazów. – Ma swój własny stos (zmienne lokalne funkcji). – Ma dostęp do przestrzeni adresowej, plików i innych zasobów procesu. Wszystkie wątki procesu współdzielą te zasoby. Procesy są od siebie izolowane, wątki nie ! Operacje zakończenia, zawieszenia procesu dotyczą wszystkich wątków. Przełączanie pomiędzy równoprawnymi wątkami jest „tanie” (szybsze) w porównaniu z przełączaniem pomiędzy tradycyjnymi procesami (nie trzeba przełączać kontekstu pamięci). Wątki zyskują na popularności ponieważ – mając pewne cechy ciężkich procesów – są efektywniejsze w działaniu.
Schemat procesu i wątków Przestrzeń adresowa Otwarte pliki Procesy potomne Obsługa sygnałów Sprawozdawczość Zmienne globalne Wątek 1 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan Wątek 2 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan Wątek 2 Licznik rozkazów Rejestry Stos i wskaźnik stosu Stan
Diagram przejść pomiędzy stanami wątku w UNIX
Proces z wątkami – w zależności od implementacji Standardowy Unix MS-DOS Linux, MS-Windows, POSIX, OS/2, Solaris
Wątki na poziomie użytkownika ang. user-level threads System operacyjny nie jest świadom istnienia wątków. Zarządzanie wątkami jest przeprowadzane przez bibliotekę w przestrzeni użytkownika. Przykład: Wątek A wywołuje funkcję read. Standardowo funkcja systemowa read jest synchroniczna (usypia do momentu zakończenia operacji). Jednak implementacja w bibliotece wywołuje wersję asynchroniczną i przełącza się do wątku B. Rozwiązanie to jest szybkie, ma jednak wady: – Dwa wątki nie mogą się wykonywać współbieżnie na dwóch różnych procesorach. – Nie można odebrać procesora jednemu wątkowi i przekazać drugiemu
Wątki na poziomie jądra ang. kernel-level threads Wątek jest jednostką systemu operacyjnego. Wątki podlegają szeregowaniu przez jądro. W systemie SMP* wątki mogą się wykonywać na różnych procesorach – przetwarzanie równoległe. Windows i Linux wykorzystują tę metodę. *) SMP (ang. Symmetric Multiprocessing, przetwarzanie symetryczne) - architektura komputerowa, która pozwala na znaczne zwiększenie mocy obliczeniowej systemu komputerowego poprzez wykorzystanie dwóch lub więcej procesorów do jednoczesnego wykonywania zadań.
Implementacja wątków w POSIX Program korzystający z funkcji operujących na wątkach POSIX musi zawierać dyrektywę #include <pthread.h> Kompilując przy użyciu gcc programy korzystające z tej biblioteki, należy wymusić jej dołączenie, przez użycie opcji -lpthread: gcc -lpthread program.c Każdy proces zawiera przynajmniej jeden główny wątek początkowy, tworzony przez system operacyjny w momencie stworzenia procesu. Aby do procesu dodać nowy wątek należy wywołać funkcję pthread_create. Nowo utworzony wątek zaczyna się od wykonania funkcji użytkownika przekazanej jako argument pthread_create. Wątek działa aż do czasu wystąpienia jednego z następujących zdarzeń: zakończenia funkcji, wywołania funkcji pthread_exit, anulowania wątku za pomocą funkcji pthread_cancel, zakończenia procesu macierzystego wątku, wywołania funkcji exec przez jeden z wątków.
Przykład użycia funkcji pthread_create Funkcja pthread_create tworzy nowy wątek. Rozpoczyna on pracę od funkcji, której adres przekazano jako trzeci argument. void *wat_fun(void *param) { // tu kod wątku // możemy przekazać wynik return NULL; } Funkcja pthread_join usypia wywołujący ją wątek do momentu, kiedy wątek o identyfikatorze przekazanym jako pierwszy argument zakończy pracę. int main() { pthread_t id; // Parametr przekazywany wątkowi void *param=NULL; pthread_create(&id,NULL,&wat_fun,param); // Funkcja thread w odrębnym wątku współbieżnie z main. // id przechowuje identyfikator wątku void *result; // Czekaj na zakończenie wątku pthread_join(id,&result); // Wynik w result, zamiast &result można przekazać NULL } Zakończenie pracy wątku – powrót z funkcji, która go rozpoczyna.
Opis funkcji systemowych pthread_create(pthread_t *thread, pthread_attr_t *attr, void*(*start_routine) (void*),void *arg) W Linuxie wątki mogą być tworzone poprzez funkcję pthread_create(). Podobnie jak fork() funkcja pthread_create() tworzy nowy kontekst wykonywania. Nowy wątek współdzieli z tworzącym go procesem PID, przestrzeń adresową, deskryptory plików itp. Wątek kończy swoje działanie w momencie kiedy skończy się wykonywanie funkcji wskazywanej przez start_routine przekazanej jako parametr do pthread_create(). Parametry funkcji wskazywanej przez start_routine czyli parametr arg muszą być przekazane przez wskaźnik na obszar pamięci (zmienną prostą lub strukturę), który zawiera odpowiednie wartości. Parametr attr wskazuje na atrybuty wątku, a przez wskaźnik thread zwracany jest identyfikator wątku.
Opis funkcji systemowych W Linuxie wątki mogą być także tworzone poprzez funkcję clone(). W rzeczywistości funkcja ta tworzy nowy proces, tak jak fork(), ale pozwala procesowi potomnemu współdzielić z procesem wywołującym część kontekstu wykonania (obszar pamięci, tablica deskryptorów plików, tablica programów obsługi sygnałów) int clone(int (*fn)(void*), void*child_stack, int flags, void*arg); Różnica pomiędzy pthread_create() a clone(). Można wskazać: które typy zasobów będą a które nie będą współdzielone (np. identyfikatory plików, uchwyty sygnałów itp.). jaki będzie posiadała PID procesu macierzystego jaki sygnał (jeśli w ogóle) będzie dostarczał informacji twórcy wątku o zakończeniu wątku . gdzie będzie umieszczony stos nowego wątku. Wątek kończy swoje działanie w momencie kiedy skończy się wykonywanie funkcji przekazanej jako parametr do clone().
Opis funkcji systemowych pthread_exit(void *retval) zakończenie wątku. Funkcja powoduje zakończenie wątku i przekazanie retval, jako wskaźnika na wynik. Wskaźnik ten może zostać przejęty przez inny wątek, który będzie wykonywał funkcję pthread_join pthread_join(pthread_t th, void **thread_return) oczekiwanie na zakończenie wątku. Funkcja umożliwia zablokowanie wątku w oczekiwaniu na zakończenie innego wątku, identyfikowanego przez parametr th. Jeśli oczekiwany wątek zakończył się wcześniej, funkcja zakończy się natychmiast. Funkcja przekazuje przez parametr thread_return wskaźnik na wynik wątku (wykonywanej przez niego funkcji), przekazany jako parametr funkcji pthread_exit wywołanej w zakończonym wątku. pthread_cancel(pthread_t thread) zakończenie wykonywania innego wątku. Funkcja umożliwia wątkowi usunięcie z systemu innego wątku, identyfikowanego przez parametr thread.
Przykład programu z wątkiem #include <pthread.h> #include <stdlib.h> #include <unistd.h> void *Hello(void *arg) { int i; for ( i=0; i<20; i++ ) { printf("Wątek pisze na ekranie!\n"); sleep(1); } return NULL; int main(void) { pthread_t mojwątek; if ( pthread_create( &mojwątek, NULL, Hello, NULL)){ printf("błąd przy tworzeniu wątku\n"); abort(); if ( pthread_join ( mojwatek, NULL ) ) { printf("błąd w kończeniu wątku\n"); exit(); return 0;
Problem synchronizacji wątków Architektura RISC: ładuj do rejestru, zwiększ wartość, zapisz wynik. Niech x oznacza jest modyfikowaną zmienną counter. Przyjmijmy, że x=5 Rozważmy dwie możliwe kolejności wykonywania instrukcji poszczególnych procesów. – a) Poprawna wartość 5. – b) Niepoprawna wartość 4. Wybór jednej z tych wielkości niedeterministyczny. Sytuacja wyścigu
Problem z synchronizacją #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> int X=0; void *fun(void *arg) { int i,j; for (i=0; i<20; i++) j=X; j++; printf("O"); fflush(stdout); sleep(1); X=j; } return NULL; int main(void) { pthread_t id_w; int i; if(pthread_create(&id_w,NULL,fun,NULL)) { printf("błąd przy tworzeniu wątku."); abort(); } for ( i=0; i<20; i++) X++; printf("X"); fflush(stdout); sleep(1); if (pthread_join(id_w, NULL)) printf("błąd przy kończeniu wątku."); printf("\nX=%d\n", X); exit(0); Przykład wyścigu
Cechy wątków Zalety Wady – Utworzenie i zakończenie wątku zajmuje znacznie mniej czasu niż w przypadku procesu – Możliwość szybkiego przełączania kontekstu pomiędzy wątkami tego samego procesu – Możliwość komunikacji wątków bez pośrednictwa systemu operacyjnego – Możliwość wykorzystania maszyn wieloprocesorowych Wady Źle zachowujący się wątek może zakłócić pracę innych wątków tego samego procesu. W przypadku dwóch procesów o odrębnych przestrzeniach adresowych nie jest to możliwe
Synchronizacja wątków Stosowane są dwie metody zapewnienia odpowiedniej koordynacji wątków: korzystanie z zamków czyli blokad wzajemnie wykluczających, tzw. muteksów korzystanie z konstrukcji nazywanych zmiennymi warunkowymi. Mutex to inaczej semafor binarny (zezwala na dostęp lub zabrania). Zamknięcia muteksu może dokonać dowolny wątek znajdujący się w jego zasięgu, natomiast otworzyć go może tylko wątek który go zamknął. Wątki, które nie mogą uzyskać dostępu do muteksu są blokowane w oczekiwaniu na niego. Operacje wykonywane na muteksach są niepodzielne. Jeśli za pomocą muteksów trzeba synchronizować wątki kilku procesów, należy odwzorować mutex w obszar pamięci współdzielonej dostępny dla wszystkich procesów.
Synchronizacja wątków Gdy niezbędne jest synchronizowanie wątków za pomocą bieżących wartości danych chronionych muteksami, można użyć konstrukcji nazywanych zmiennymi warunkowymi. Zmienna warunkowa jest kojarzona z konkretnym muteksem i predykatem. Podobnie jak mutex może ona być odwzorowana w pamięć współdzieloną, dzięki czemu może być używana przez parę procesów. Głównym zadaniem zmiennych warunkowych jest powiadamianie innych procesów o tym, że dany warunek został spełniony, lub do blokowania procesu w oczekiwaniu na otrzymanie powiadomienia. W momencie kiedy wątek jest blokowany na zmiennej warunkowej, skojarzony z nim mutex jest zwalniany. W oczekiwaniu na to samo powiadomienie zablokowanych może być kilka wątków. Wątek zgłasza powiadamianie wysyłając sygnał do skojarzonej zmiennej warunkowej.
Opis funkcji systemowych Do zapewnienia wzajemnego wykluczania używana jest zmienna (np. mutex), zadeklarowana następująco: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(pthread_mutex_t *mutex) zajęcie zamka (zajęcie sekcji krytycznej). Funkcja powoduje zajęcie zamka wskazywanego przez parametr mutex, poprzedzone ewentualnym zablokowaniem wątku do czasu zwolnienia zamka, jeśli został on wcześniej zajęty przez inny wątek. pthread_mutex_unlock(pthread_mutex_t *mutex) zwolnienie zamka (zwolnienie sekcji krytycznej). Funkcja powoduje zwolnienie zamka wskazywanego przez parametr mutex, umożliwiając jego zajęcie innemu wątkowi. pthread_mutex_trylock(pthread_mutex_t *mutex) próba zajęcia zamka. Funkcja powoduje zajęcie zamka wskazywanego przez parametr mutex, jeśli nie jest zajęty przez inny wątek. W przeciwnym przypadku zwraca błąd, nie blokując tym samym procesu.
Opis funkcji systemowych Synchronizacja za pomocą zmiennych warunkowych polega na usypianiu i budzeniu wątku w sekcji krytycznej. W tym celu używana jest zmienna warunkowa (tu cond), zadeklarowana następująco: pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) oczekiwanie na sygnał. Funkcja powoduje uśpienie wątku na zmiennej warunkowej, wskazywanej przez parametr cond. Na czas uśpienia wątek zwalnia zamek, wskazywany przez parametr mutex, udostępniając tym samym sekcję krytyczną innym wątkom. Po obudzeniu i wyjściu z funkcji (na skutek odebrania sygnału wysłanego przez pthread_cond_signal) zamek zajmowany jest ponownie. pthread_cond_signal(pthread_cond_t *cond) wysłanie sygnału (obudzenie) do jednego z wątków oczekujących na zmiennej warunkowej wskazywanej przez cond.
Przykład synchronizacji z muteksem #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> int X; pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; void *fun(void *arg) { int i,j; for ( i=0; i<20; i++ ) pthread_mutex_lock(&mut); j = X; j++; printf("O"); fflush(stdout); sleep(1); X = j; pthread_mutex_unlock(&m); } return NULL; int main(void) { pthread_t id_w; int i; if(pthread_create(&id_w,NULL,fun,NULL)) { printf("błąd tworzenia wątku."); abort(); } for ( i=0; i<20; i++) { pthread_mutex_lock(&mut); X++; pthread_mutex_unlock(&mut); printf("o"); fflush(stdout); sleep(1); if(pthread_join(id_w, NULL ) ) { printf("błąd kończenia wątku."); printf("\nX = %d\n", X); exit(0);
Możliwe problemy z wątkami Przykład: errno to uniksowy mechanizm zgłaszania błędów przez funkcje libc, a w szczególności jądra. Jeśli funkcja zakończy się błędem, sygnalizuje to zwracając zwykle -1 lub NULL. Program powinien wtedy zajrzeć do zmiennej globalnej errno, żeby dowiedzieć się jaki dokładnie błąd wystąpił. Jeśli funkcja zakończy się pomyślnie zawartość errno nie jest zdefiniowana.
Możliwe problemy z wątkami #include <stdio.h> /* fprintf */ #include <errno.h> /* errno */ #include <stdlib.h> /* malloc, free, exit */ #include <string.h> /* strerror */ extern int errno; int main( void ) { /* deklaracja wskaźnika do tablicy o pojemności 2GB */ char *ptr = malloc( 2000000000UL ); if ( ptr == NULL ){ puts("malloc failed"); puts(strerror(errno)); } else /* Pomyślnie zaalokowano tablicę free( ptr ); exit(EXIT_SUCCESS); /* exiting program */
Możliwe problemy z wątkami W standardowej bibliotece C, w wersji wielowątkowej, errno jest implementowane jako prywatna zmienna globalna (nie współdzielona z innymi wątkami) Gdy kilka wątków jednocześnie wywołuje funkcje malloc/free - może dojść do uszkodzenia globalnych struktur danych (listy wolnych bloków pamięci) Potrzeba synchronizacji => może prowadzić do spadku wydajności
Inne problemy z wątkami Proces otrzymuje sygnał: – Wszystkie wątki otrzymują sygnał – Wybrany wątek otrzymuje sygnał – Wątek aktualnie aktywny otrzymuje sygnał Proces wykonuje fork. – Czy duplikować jedynie działający wątek, czy też wszystkie wątki ? Proces wywołuje exit. – Zakończyć proces czy też jedynie aktywny wątek ? Anulowanie wątku (ang. cancellation). – Wykonać natychmiast . – Wątek co jakiś czas sprawdza czy nie został anulowany
Literatura Ważniak – laboratorium z systemów operacyjnych http://wazniak.mimuw.edu.pl/index.php?title=Systemy_operacyjne#Laboratorium Wojciech Kwedlo, Wykład z Systemów Operacyjnych Wydział Informatyki Politechniki Białostockiej http://aragorn.pb.bialystok.pl/~wkwedlo/OS-Slides-new.html