Plan wykładu Obliczenia równoległe w zagadnieniach inżynierskich Wykład 1 Podstawowe pojęcia i model programowania Sposoby realizacji watków w systemach operacyjnych Tworzenie watów i zarzadzanie nimi Sposoby realizacji synchronizacji watków Metody rozwiazań przykładowych problemów Dr inż. Tomasz Olas olas@icis.pcz.pl Instytut Informatyki Teoretycznej i Stosowanej Politechnika Częstochowska 1/43 Wykład 1 p Procesy współbieżne Wzajemne wykluczanie Mówimy, że dwa procesy sa współbieżne, jeśli jeden z nich rozpoczyna się przed zakończeniem drugiego. W systemach jednoprocesorowych czas pracy procesora jest dzielony pomiędzy wszystkie wykonywane współbieżnie procesy poprzez wykorzystanie zasady podziału czasu. Jeśli w systemie komputerowym jest wiele procesorów, moga one wykonywać różne procesy jednocześnie. Uzależnione procesy moga ze soba współpracować lub współzawodniczyć. Praca takich procesów wymaga synchronizacji. Obiekt, z którego może korzystać w sposób wyłaczny wiele procesów, nazywa się zasobem dzielonym. Fragment procesu w którym korzysta on z obiektu dzielonego, nazywa się sekcja krytyczna tego procesu. Problem wzajemnego wykluczania: zsynchronizować N procesów, z których każdy w nieskończonej pętli na przemian zajmuje się własnymi sprawami i wykonuje sekcję krytyczna, w taki sposób, aby wykonanie jakichkolwiek dwóch lub więcej procesów nie pokrywało się w czasie. Rozwiazanie problemu wzajemnego wykluczania - dodanie instrukcji poprzedzajacych sekcję krytyczna (protokół wstępny), oraz następujacych bezpośrednio po sekcji krytycznej (protokół końcowy). 3/43 Wykład 1 p
Bezpieczeństwo i żywotność Blokada i zagłodzenie Poprawność programu sekwencyjnego: częściowa poprawność - jeśli się zatrzyma, to zwróci dobre wyniki, własność stopu - w ogóle się zatrzyma. Własność bezpieczeństwa - program współbieżny jest bezpieczny jeśli nigdy nie doprowadza do niepożadanego stanu (nigdy swa procesy nie znajda się jednocześnie w swoich sekcjach krytycznych). Własność żywotności zapewnia, że każde pożadane zdarzenie w końcu zajdzie (jeśli jakiś proces czeka na wejście do swojej sekcji krytycznej, do w końcu do niej wejdzie). Sprawiedliwość. Blokada (zastój, zakleszczenie lub martwy punkt) - Zbiór procesów znajduje się w stanie blokady, jeśli każdy z tych procesów jest wstrzymywany w oczekiwaniu na zdarzenie, które może być spowodowane przez jakiś inny proces z tego zbioru. Zagłodzenie (wykluczenie) - proces nie zostaje wznowiony, mimo że zdarzenie na które czeka, występuje dowolna ilość razy (za każdym razem gdy proces ten mógłby być wznowiony, jest wybierany jakiś inny proces). 5/43 Wykład 1 p Watek - definicja Własności watków Watek (thread) można określić jako pojedyncza sekwencję sterowania wewnatrz procesu (podstawowa jednostka użycia procesora). Watek wykonuje niezależny ciag instrukcji, który może być szeregowany do wykonania przez system operacyjny. Środowiskiem do wykonywania watku jest proces. Tradycyjna implementacja procesu ma jeden watek sterowania. W nowszych systemach dopuszcza się istnienie wielu watków wewnatrz procesu. Koszt utworzenia i przełaczenia watku jest mniejszy niż procesu. Dane statyczne procesu sa dla watków działajacych w ramach jednego procesu wzajemnie widoczne. Wykonanie każdego watku przebiega sekwencyjnie; każdy watek ma swój licznik rozkazów. Watki moga być wykonywane na oddzielnych procesorach, co umożliwia przyspieszenie obliczeń. Ponieważ watki dziela wspólne dane konieczna jest synchronizacja dostępu do tych wspólnych danych. 7/43 Wykład 1 p
Typy watków Biblioteka Pthreads Ze względu na sposób implementacji rozróżnia się następujace typy watków: Watki poziomu jadra (kernel-space threads) sa implementowane poprzez dołaczenie do każdego procesu tabeli jego watków. System zarzadza każdym watkiem wykorzystujac kwant czasu przyznany dla jego procesu rodzica (funkcja clone). Watki poziomu użytkownika (user-space threads). Rezygnacja z zarzadzania watkami przez jadro. W procesie jest definiowany zbiór wykonalnych procedur, które sa wymieniane poprzez operacje na wskaźniku stosu. Dwupoziomowy (hybrydowy) system watków (two-level threads). Połaczenie systemu watków poziomu użytkownika i jadra. Zestaw funkcji dotyczacy watków zdefiniowany został przez normę POSIX P1003.4a i nosi nazwę Pthreads (skrót od POSIX threads). Jest to zbiór typów i funkcji języka C. Implementacja pakietu istnieje między innymi w systemach Linux, QNX6, DEC OSF1. Obecnie watki sa częścia biblioteki glibc (od wersji 2). 9/43 Zasoby watku Watki POSIX - grupy funkcji Watek korzysta z zasobów procesu, ale może być szeregowany do wykonania jako niezależna jednostka w ramach procesu. Ma swój własny przebieg i własne zasoby lokalne: stos, rejestry, sposób kolejkowania (szeregowania): np. priorytet, zbiór sygnałów, lokalne dane watku. Pozostałe zasoby watki dziela ze soba w ramach procesu - pamięć, instrukcje programu, ID procesu, deskryptory plików, dzielone biblioteki, mechanizmy komunikacji międzyprocesorowej, itd. Funkcje realizujace watki POSIX można podzielić na trzy grupy: Zarzadzanie watkami - funkcje do tworzenia, zarzadzania, usuwania watków, oraz funkcje zwiazane z atrybutami watków. Obiekty mutex - funkcje realizujace synchronizacje dostępu do zasobów ( MUTual EXclusion - wzajemne wykluczanie). Funkcje zapewniaja tworzenie, usuwanie, otwieranie i zamykanie obiektów mutex. Zmienne warunkowe - funkcje realizujace komunikację między watkami dzielacymi obiekty mutex. Wykorzystuja warunki zdefiniowane przez programistę. Sa to funkcje do tworzenia, usuwania, czekania i wysyłania sygnału przy określonej wartości zmiennej. 11/43
Konwencja nazw Tworzenie watku (I) Przedrostek funkcji pthread pthread_attr pthread_mutex pthread_mutexattr pthread_cond pthread_condattr pthread_key Grupa funkcji Funkcje watków oraz funkcje pomocnicze Atrybuty watków Obiekty mutex Atrybuty obiektów mutex Zmienne warunkowe Atrybuty zmiennych warunkowych Specyficzne dla watku klucze (dane lokalne) Do uruchomienia nowego watku służy funkcja pthread_create: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void* (* func)(void *), void *arg) Utworzony watek wykonuje kod funkcji fun, której adres został przekazany poprzez trzeci parametr wywołania funkcji. Do funkcji wykonywanej przez watek można przekazać dane za pomoca parametru arg. Poprzez pierwszy parametr zwracany jest identyfikator watku (jest on wykorzystywany do określania watku w innych funkcjach standardu pthreads). 13/43 Tworzenie watku (II) Zakończenie działania watku Funkcja wykonywana przez watek powinna mieć postać: void *f(void *arg); Przykład: void *run(void *arg) int main(int argc, char** argv) pthread_t threadid; if (pthread_create(&threadid, NULL, run, NULL)) std::cerr << "błąd podczas tworzenia wątku" << std::endl; Watek może być zakończony w następujacy sposób: Następuje powrót z funkcji wykonywanej przez watek, Watek wywoła funkcje pthread_exit(). Watek zostaje odwołany przez inny watek za pomoca funkcji pthread_cancel(), Cały proces zostaje zakończony przez wywołanie funkcji exit() czy exec(), lub watek główny zostanie zakończony poprzez wywołanie return w funkcji main(). 15/43
Kończenie watku - zasoby Oczekiwanie na zakończenie watku Możliwe sa dwa sposoby postępowania z zasobami zakończonych watków: Z chwila zakończenia watku zwalniane sa wszystkie jego zasoby. Zasoby zwalniane sa z chwila dołaczenia bieżacego watku do innego watku (który wywołał funkcję pthread_join). Ustawienie sposobu postępowania z zasobami watków po ich zakończeniu jest możliwe poprzez atrybuty watku lub za pomoca funkcji pthread_detach(). Watek może oczekiwać na zakończenie działania innego watku przez wywołanie funkcji pthread_join. int pthread_join(pthread_t thread_id, void **thread_return) thread_id - identyfikator watku na zakończenie którego będzie czekał wołajacy watek, thread_return - jeśli jest różny od NULL, to wówczas kod zakończenia watku thid zostanie wstawiony pod adres wskazywany przez thread_return. Kodem zakończenia może być też wartość określona przy wołaniu funkcji pthread_exit lub PTHREAD_CANCELLED jeśli watek został usunięty. Watek do którego jest dołaczany dany watek musi być w stanie umożliwiajacym dołaczenie. Funkcja pthread_join powinna być wykonana dla każdego nie odłaczonego watku. 17/43 Zasoby watków - przykład Tworzenie watków - przykład (I) pthread_create() Zakonczenie pthread_exit() pthread_join() W1 i W2 ustawiony atrybut PTHREAD_CREATE_JOINABLE Zwolnienie zasobów W3 i W4 ustawiony atrybut PTHREAD_CREATE_DETACHED Zakonczenie i zwolnienie zasobów #include <pthread.h> #include <unistd.h> #include <iostream> void* NewThread(void* arg) int id = *static_cast<int*>(arg); for (int i = 0; i < 3; i++) std::cout << id << " " << std::flush; sleep(1); return NULL; 19/43
Tworzenie watków - przykład (II) Tworzenie watków - przykład (III) int main() pthread_t thread; int id1 = 1; if (pthread_create(&thread, NULL, NewThread, (void *)(&id1))) std::err << "błąd podczas tworzenia wątku nr 1" << std::endl; pthread_detach(thread); Kompilacja: g++ -o watki watki.cpp -lpthread lub g++ -pthread -o watki watki.cpp Wynik działania programu: 1 2 1 2 1 2 int id2 = 2; if (pthread_create(&thread, NULL, NewThread, (void *)(&id2))) std::cerr << "błąd podczas tworzenia wątku nr 2" << std::endl; pthread_detach(thread); pthread_exit(null); 21/43 Funkcja pthread_join - przykład (I) Funkcja pthread_join - przykład (II) #include <pthread.h> #include <iostream> #include <unistd.h> #include <fstream> const int size = ; void * SaveThread(void * arg) std::cout << "Begin Thread" << std::endl; ofstream os("ala.txt"); for (int i = 0; i < size; i++) os << "0"; std::cout << "End Thread" << std::endl; return NULL; int main() std::cout << "Begin Program" << std::endl; pthread_t thread; if (pthread_create(&thread, NULL, SaveThread, NULL)) cerr << "błąd podczas tworzenia wątku" << std::endl; for (int i = 0; i < 30; i++) sleep(1); std::cout << i << " " << flush; void* result; pthread_join(thread, &result); std::cout << std::endl << "End program \n" << std::endl; pthread_exit(null); 23/43
Funkcja pthread_join - przykład (III) Obiekty mutex Wynik działania programu dla size = 50000000: Begin Program Begin Thread 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 End Thread End program Wynik działania programu dla size = 5000000: Begin Program Begin Thread 0 1 2 3 4 5 End Thread 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 End program Mutex jest mechanizmem wzajemnego wykluczania ( MUTual EXclusion ) służacych do ochrony danych wspólnych dla watków przed jednoczesnymi modyfikacjami. Mechanizm ten może służyć do implementacji sekcji krytycznych, semaforów i monitorów. Mutex ma dwa stany: otwarty - nie zajęty przez żaden watek, zajęty - zajęty przez jeden watek. Mutex nie może być jednocześnie zajęty przez więcej niż jeden watek. Watek próbujacy zajać już zajęty watek zostaje wstrzymany do chwili zwolnienia mutexu przez watek, który go zajał wcześniej. 25/43 Zajęcie i zwolnienie obiektu mutex Obiekty mutex - przykład Na obiekcie mutex wykonuje się dwie podstawowe operacje: zajęcie i zwolnienie obiektu mutex. Do zajęcia obiektu mutex służy funkcja pthread_mutex_lock, natomiast do zwolnienia funkcja pthread_mutex_unlock. Jako parametr przyjmuja one wskaźnik do wcześniej utworzonego obiektu mutex. Thread 1 Thread 2 mutex_lock(mutex) uzycie zasobu mutex_unlock(mutex) mutex_lock(mutex) uzycie zasobu blokada odblokowanie // utworzenie i zainicjowanie muteksu pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void * run(void * arg) pthread_mutex_lock(&mutex); // zajęcie muteksu - protokół wstępny // operacje na zasobie dzielonym - sekcja krytyczna pthread_mutex_unlock(&mutex); // zwolnienie muteksu- protokół końcowy mutex_unlock(mutex) 27/43
Operacje na obiektach mutex - przykład (I) Operacje na obiektach mutex - przykład (II) #include <pthread.h> #include <iostream> #include <unistd.h> const int size = 100; int sum = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void * ForThread(void * arg) int* tab = static_cast< int* >(arg); for (int i = 0; i < size/2; i++) pthread_mutex_lock(&mutex); sum += tab[i]; pthread_mutex_unlock(&mutex); return NULL; 29/43 int main() pthread_t thread1, thread2; int tab[size]; for (int j = 0; j < size; j++) tab[j] = j; if (pthread_create(&thread1, NULL, ForThread, tab)) std::cerr << "błąd podczas tworzenia wątku" << std::endl; if (pthread_create(&thread2, NULL, ForThread, tab+size/2)) std::cerr << "błąd podczas tworzenia wątku" << std::endl; pthread_join(thread1, NULL); pthread_join(thread2, NULL); std::cout << "Suma: " << sum << std::endl; return 0; Zmienne warunkowe (I) Oczekiwanie na spełnienie warunku Zmienne warunkowe sa mechanizmem umożliwiajacym zawieszenie i zwolnienie czasu procesora (watku) do momentu, w którym zostanie spełniony określony warunek. Warunek ten może być dowolny i niezależny od zmiennej warunkowej, np. osiagnięcie przez zmienna określonej wartości. Zmienna warunkowa musi być zawsze otoczona obiektem mutex, aby uniknać jednoczesnej próby oczekiwania i sygnalizowania na zmiennej warunkowej. Przed wykorzystaniem zmienna warunkowa musi zostać odpowiednio zainicjowana. Do oczekiwania na spełnienie warunku służy funkcja pthread_cond_wait. Funkcja pthread_cond_wait w sposób atomowy zwalnia mutex (tak jak funkcja pthread_mutex_unlock) i oczekuje na sygnał o spełnienie zmiennej warunkowej cond. Wykonanie watku jest zawieszone i nie zajmuje on czasu procesora aż do momentu odebrania sygnału od zmiennej warunkowej. Mutex musi być zajęty przez watek wywołujacy pthread_cond_wait. Przed końcem działania pthread_cond_wait zajmuje mutex. 31/43
Zmienne warunkowe - przykład użycia Zmienne warunkowe - przykład (I) Watek 1 (oczekujacy na warunek): pthread_mutex_lock(&m); pthread_cond_wait(&cond, &m); pthread_mutex_unlock(&m); Watek 2 (sygnalizujacy spełnienie warunku): pthread_mutex_lock(&m); pthread_cond_signal(&cond); pthread_mutex_unlock(&m); blokada odblokowanie Watek 1 Watek 2 mutex_lock(m) cond_wait(c,m) uzycie zasobu mutex_unlock(m) mutex_lock(m) cond_signal(c) mutex_unlock(m) blokada odblokowanie ustawienie warunku odblokowanie int number; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* RandomThread(void* arg) srandom(1); for (int i=0; i<20; i++) pthread_mutex_lock(&mutex); number = static_cast<double>(rand())/rand_max*10; if (number < 5) std::cout << "mniejsza"; pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mutex); sleep(1); 33/43 Zmienne warunkowe - przykład (II) Zmienne warunkowe - przykład (III) void* OutputThread(void* arg) while (true) pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); std::cout << "Wygenerowana liczba - " << number << " jest mniejsza od 5" << std::endl; pthread_mutex_unlock(&mutex); int main() pthread_t thread1; if (pthread_create(&thread1, NULL, RandomThread, NULL)) std::cerr << "błąd podczas tworzenia wątku" << std::endl; pthread_t thread2; if (pthread_create(&thread2, NULL, OutputThread, NULL)) std::cerr << "błąd podczas tworzenia wątku" << std::endl; void* result; pthread_join(thread1, &result); pthread_cancel(thread2); pthread_exit(null); 35/43
Jednokrotne wykonanie Jednokrotna inicjalizacja - przykład (I) Wywołanie funkcji pthread_once daje pewność, że dany kod zostanie wykonany tylko przez jeden (pierwszy) watek, pomimo, że funkcja ta będzie wywoływana przez wiele watków. Funkcja ta może zostać wykorzystana np. do inicjalizacji zmiennych wspólnych dla watków. int pthread_once(pthread_once_t *once_control, void (*init_routine) (void)); once_control - zainicjowana zmienna typu pthread_once_t, init_routine - funkcja jaka ma zostać wykonana. Zmienna once_control musi być przed użyciem zainicjowana: pthread_once_t once_control = PTHREAD_ONCE_INIT; #include <pthread.h> #include <iostream.h> #include <unistd.h> pthread_once_t initcontrol = PTHREAD_ONCE_INIT; void Init() std::cout << "Initializacja zostala wykonana " << "przez watek o identyfikatorze " << pthread_self() << std::endl; void * Thread(void * arg) std::cout << "Thread - " << pthread_self() << std::endl; pthread_once(&initcontrol, Init); return NULL; 37/43 Jednokrotna inicjalizacja - przykład (II) Jednokrotna inicjalizacja - pętla int main() pthread_t thread[3]; for (int i = 0; i < 3; i++) if (pthread_create(thread + i, NULL, Thread, NULL)) for (int i = 0; i < 3; i++) pthread_join(thread + i, NULL); return 0; Wynik działania programu: Thread - 1026 Initializacja zostala wykonana przez watek o identyfikatorze 1026 Thread - 2051 Thread - 3074 39/43 bool init = true; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* run(void * arg) pthread_mutex_lock(&mutex); if (!init) // kod wykonywany tylko przez jeden wątek init = false; else init = true; pthread_mutex_unlock(&mutex);
Bramki Bramki - przykładowa implementacja (I) Poczawszy od wersji IEEE Std 1003.1-2001 standardu wprowadzono funkcje implementujace bramki (barrier). Biblioteka glibc posiada implementacje bramek poczawszy od wersji 2.2. Synchronizacja przy użyciu bramek polega na wstrzymaniu watków aż do momentu, w którym wszystkie watki osiagn a dany punkt synchronizacji (służy do tego funkcja pthread_barrier_wait). Poniżej przedstawiono przykładowa implementację bramek przy użyciu zmiennych warunkowych (bramek oczywiście nie trzeba implementować - sa już w standardzie): struct pthread_barrier_t int nthreads; // liczba watkow do wstrzymywania pthread_mutex_t mutex; pthread_cond_t cond; int nwaiting; // liczba aktualnie czekajacych watkow ; 41/43 inline int pthread_barrier_init(pthread_barrier_t* barrier, void*, int nthreads) barrier->nthreads = nthreads; barrier->nwaiting = nthreads - 1; pthread_mutex_init(&barrier->mutex, NULL); pthread_cond_init(&barrier->cond, NULL); return 0; Bramki - przykładowa implementacja (II) inline void pthread_barrier_destroy(pthread_barrier_t* barrier) pthread_mutex_destroy(&barrier->mutex); pthread_cond_destroy(&barrier->cond); inline void pthread_barrier_wait(pthread_barrier_t* barrier) pthread_mutex_lock(&barrier->mutex); if (barrier->nwaiting) barrier->nwaiting--; pthread_cond_wait(&barrier->cond, &barrier->mutex); else barrier->nwaiting = barrier->nthreads - 1; pthread_cond_broadcast(&barrier->cond); pthread_mutex_unlock(&barrier->mutex); 43/43