Wątki i synchronizacja
Transkrypt
Wątki i synchronizacja
Projektowanie oprogramowania systemów WĄTKI I SYNCHRONIZACJA plan Wątki Właściwości Tworzenie Wzorce i łączenie zastosowań biblioteka OpenMP Synchronizacja Obiekty synchronizacji Wzorzec Monitor Właściwości wątków “wątek wykonania” – działająca ścieżka instrukcji kodu (najmniejsza sekwencja instrukcji, która może być niezależnie zarządzana przez scheduler) Każdy proces posiada przynajmniej 1 wątek (wątek główny, główna pętla programu) Dodatkowe wątki mogą być tworzone na żądanie aby wykonywać specyficzne zadania (wątki robocze – worker threads) Biblioteki i system mogą tworzyć dodatkowe wątki, działające niezależnie od kodu użytkownika, które czynią aplikację wielowątkową Wątki vs procesy Procesy są niezależne, wątki istnieją wewnątrz procesów Procesy mają większą konfigurację stanu niż wątki, które współdzielą stan procesu, jego pamięć i zasoby Procesu mają osobną przestrzeń adresową, wątki współdzielą przestrzeń adresową Procesy wchodzą w interakcje między sobą poprzez mechanizmy IPC systemu Przełączenie kontekstu pomiędzy wątkami tego samego procesu jest szybsze niż pomiędzy procesami Wielowątkowość – po co? Responsywność – przeprowadzaj długie, blokujące operacje w wątkach roboczych, aby aplikacja pozostała responsywna dla użytkownika i nie wyglądała na zawieszoną Podobny efekt można uzyskać za pomocą nieblokującego wejścia/wyjścia, bez wielowątkowości, ale jest to bardziej podatne na błędy programistyczne, trudniejsze i mniej naturalne Wydajność – na systemach wielordzeniowych wątki pozwalają uzyskać wynik szybciej poprzez podział pracy na części wykonywane na osobnych rdzeniach Przepływność/utylizacja – aplikacje wielowątkowe umożliwiają lepsze wykorzystanie systemu poprzez wykonywanie pracy w wątkach podczas gdy inne są zablokowane oczekując na I/O Wielowątkowość niebezpieczeństwa Synchronizacja – wiele wątków może równocześnie modyfikować te same dane, prowadząc do niespodziewanych efektów Wyścigi (race condition) – działanie programu zależy od określonej w czasie kolejności działania wątków. Bez odpowiedniej synchronizacji wątków, ich zależności czasowe mogą być niedeterministyczne (zwłaszcza w systemach wieloprocesorowych) Zakleszczenie (deadlock) – niewłaściwe użycie obiektów synchronizacji może prowadzić do sytuacji, kiedy wątek A uzyskał zasób a i czeka aż wątek B zwolni zasób b, podczas gdy wątek B uzyskał zasób b i czeka aż wątek A zwolni zasób a Stabilność – wadliwy wątek prowadzi do „wywalenia się” całego procesu Wyścigi Efekt oczekiwany Efekt możliwy Te sytuacje prowadzą do bardzo trudnych do zdiagnozowania bugów (Heisenbugs) Rozwiązaniem jest użycie obiektów wzajemnej wyłączności (mutual exclusion – mutex) lub innych obiektów synchronizacji (np. monitor) lub operacji atomowych Deadlock Niezbędne jest zachowywanie tej samej sekwencji akwizycji zasobów w obu wątkach lub zastosowanie „bezpiecznych” wzorców – np. monitor Cykl życia wątku Poza wątkiem głównym, dodatkowe wątki są powoływane do życia jawnie, poprzez wywołanie systemowe (funkcję systemu operacyjnego) Wątek może być stworzony w stanie wstrzymanym lub działającym, niektóre systemy umożliwiają również wstrzymanie działającego wątku (czego należy unikać bo prowadzi do deadlock-a) Aby utworzyć wątek potrzebujemy funkcji wątku, która będzie stanowić jego ścieżkę wykonania kodu Kiedy funkcja wątku kończy się, wątek staje się joinable (złączalny??) Wątek może zostać złączony (joined) w dowolnym momencie, ale złączenie (join) będzie wstrzymany aż wątek stanie się joinable Złączenie (join) wątku kończy jego istnienie i zwalnia zasoby Na Windows użyj API _beginthread()/_beginthreadex() aby stworzyć wątek (lub funkcję CreateThread()); WaitForSingleObject() lub inne funkcje oczekiwania aby złączyć (join) go Na POSIX-ach użyj API phtread_create() & pthread_join() (patrz man 3 pthread_create) Najlepiej: używaj standardu C++11 i klasy std::thread z nagłówka <thread> Pula wątków Ogólny wzorzec użytkowy, w którym tworzymy wiele wątków, a zadania do wykonania kolejkujemy Wątki robocze pobierają zadania z kolejki, przetwarzają je i zachowują wynik Po zakończeniu zadania wątek powraca do puli i oczekuje na kolejne zadanie (lub natychmiast dostaje zadanie, jeśli już jest w kolejce) Zadania (i ich czas wykonania) mogą być identyczne lub różne – możliwe optymalizacje Przepływność systemu się zwiększa (całkowity czas wykonania wszystkich zadań się zmniejsza) Liczba wątków może odpowiadać liczbie rdzeni procesora (dlaczego?) Bezpieczeństwo ze względu na wątki (thread safety) Fragment kodu jest thread-safe jeżeli manipuluje współdzielonymi strukturami danych w sposób, który gwarantuje bezpieczne wykonanie przez wiele wątków na raz (gwarancja braku wyścigów) Rozwiązania Unikaj współdzielonych danych Re-entrancy – pisz kod w taki sposób, żeby nie wiązało się to z przechowywaniem stanu w zmiennych globalnych/dzielonych. Dostęp do nie-lokalnego stanu odbywa się za pomocą operacji atomowych Thread-local storage – każdy wątek ma własną kopię danych Synchronizuj dostęp do współdzielonych danych Mutual exclusion (wzajemna wyłączność) – dostęp do danych jest szeregowany za pomocą obiektów synchronizacji zapewniających że tylko jeden wątek czyta/zapisuje dane na raz Operacje atomowe – użycie specjalnych instrukcji, które nie mogą być przerwane przez inne wątki Stan niezmienny – po utworzeniu, stan obiektu nie może się zmienić Thread local storage Niektóre systemy lub języki programowania pozwalają tworzyć zmienne, których wartość może być różna w każdym wątku – każdy wątek otrzymuje inną kopię tej samej zmiennej Najbardziej popularnym przykładem jest zmienna errno (kod ostatniego błędu) ze standardowej biblioteki języka C – gdyby istniała tylko 1 zmienna errno dla wszystkich wątków mielibyśmy łatwo do czynienia z wyścigami (operacje z różnych wątków nadpisują swoje kody błędu) Jak używać? C++11 – słowo kluczowe thread_local używane z globalnymi/statycznymi zmiennymi MSVC – deklarator _declspec(thread) dla zmiennych GNU C – deklarator __thread POSIX – pthread_key_create()/pthread_setspecific()/pthread_key_delete() Windows – TlsAlloc()/TlsSetValue()/TlsGetValue()/TlsFree() Operacje atomowe Procesory posiadają instrukcje, które są nieprzerywalne, tzn. ich wykonanie chwilowo wstrzymuje przerwania sprzętowe (które są używane do przełączania wątków) – gwarancja, że instrukcja zakończy się deterministycznym wynikiem na systemach jednoprocesorowych Niektóre procesory posiadają również instrukcje, które uniemożliwiają innym procesorom równoczesną modyfikację tych samych komórek pamięci – kosztem spadku wydajności wynikającego z wyczyszczenia cache procesora Każda operacja może być zmieniona w atomową poprzez zamknięcie w sekcji krytycznej – dodanie obiektów synchronizacji wzajemnego wykluczenia (mutex) uniemożliwiających innym wątkom równoczesne wykonanie W C++11 używaj szablonu klasy std::atomic<> aby tworzyć atomowe typy podstawowe Kod bezpieczny ze względu na wątki Atomowość poprzez Bezpieczny ale nie Atomowa zmienna synchronizację reentrant – globalne globalna (mutual exclusion) zmienne chronione przez mutex Wzorzec singleton i problem równoczesnej inicjalizacji Singleton jest wzorcem projektowym, który ogranicza klasę do stworzenia pojedynczej instancji obiektu (AKA There can be only one) – co jest często pożądane Wprowadza to do programu stan globalny Zmienna globalna + wielowątkowość = problemy Co stanie się kiedy kod inicjujący instancję singletona jest wykonany równocześnie przez wiele wątków? (AKA concurrent initialization problem – odnosi się to do wszystkich zmiennych globalnych) Problem równoczesnej inicjalizacji „Naiwny” singleton Działający singleton Biblioteka OpenMP Open Multi-Processing – wieloplatformowe, ustandaryzowane API do tworzenia aplikacji wielowątkowych Zwłaszcza do tworzenia wysokowydajnych programów do |mielenia numerków” Zalety Przenośność (Portability) – nie trzeba znać API wątków specyficznego dla danej platformy (jak pthreads albo Windows threads) Proste API w porównaniu do natywnych Ten sam kod może działać jako szeregowy lub równoległy w zależności od konfiguracji środowiska OpenMP, nie trzeba zmieniać projektu aplikacji Wady Proste – nie tak wyrafinowane API jak natywne Brak możliwości obsługi błędów OpenMP – program się po prostu „wywala” Brak jawnego użycia obiektów synchronizacji – trudne do wykrycia bugi OpenMP – keidy używać? Gdy dekomponujemy problem polegający na wykonaniu tego samego zadania na różnych partycjach danych (data parallelism) Przykład: przeprowadź filtrację wielu kanałów danych dźwiękowych równocześnie – użycie #pragma omp parallel for Konstrukcje OpenMP Podział/zrównoleglenie pracy – tworzenie równoległych pętli i dystrybucja sekcji szeregowego kodu do wątków Podział/zdrównoleglenie danych – oznaczenie zmiennych jako współdzielonych albo prywatnych dla wątków Map/reduce – określenie zmiennych odbierających wynik redukcji danych z wątków i operacji redukcji Synchronizacja – tworzenie sekcji krytycznych, operacje atomowe & bariery wątków Szeregowanie – określenie modelu szeregowania dla równoległych pętli Zrównoleglenie warunkowe Detekcja liczby procesorów, funkcje do mierzenia czasu Więcej o OpenMP… Rozszerzenia OpenMP stanowią w zasadzie osobny język programowania, który nie jest trywialny W OpenMP nie ma niczego, czego by się nie dało zrobić za pomocą natywnych API, ale czasem OpenMP sprawia, że jest to: Łatwiejsze i szybsze w implementacji Mniej czytelne i trudniejsze do zrozumienia OpenMP pozwala uruchamiać ten sam kod na procesorach CPU i platformach GPGPU – „domowy superkomputer” (patrz również OpenCL…) OpenMP najlepiej sprawdza się przy problemach zawstydzająco równoległych (embarrassingly parallel), bardziej złożone zadania lepiej zostawić dla specjalnie zaprojektowanych modeli używających przenośnych narzędzi wątków jak nagłówek <thread> z C++11 lub biblioteka boost.threads Obiekty synchroniozacji Mutex Semaphore Condition variable Monitor (właściwie nie pojedynczy obiekt) Barrier Read/Write Lock Event (Windows) Mutex MUTual EXclusion object Podstawowe narzędzie tworzenia sekcji krytycznych Tylko jeden wątek może przejąć własność mutexa Inne wątki usiłujące przejąć własność mutexa będą oczekiwać aż pierwszy wątek odda własność Operacje: acquire (lock) release (unlock) try acquire (try lock) – zwraca wartość logiczną, czy własność została przekazana Mutex Rodzaje Mutexów W odniesieniu do granic procesów Inter-process – używany do IPC, umożliwia blokowanie wątków należących do różnych procesów (POSIX: pthread_mutex_create(); Windows: CreateMutex()) Intra-process – „tani” mutex do używania wewnątrz jednego procesu (Windows: InitializeCriticalSection()) W odniesieniu do rekurencji “zwykły” – zgłosi błąd kiedy ten sam wątek będzie chciał wejść w mutex ponownie rekursywny – pozwoli na wejście kilka razy (i będzie wymagał tyle samo odblokowań) – tylko takie są dostępne natywnie na Windows wzorzec “Scoped lock” (C++) Użycie funkcji lock()/unlock() mutexu jest zwykle niebezpieczne (lub niewygodne) w przypadku kodu z wyjątkami W takiej sytuacji sprawdzi się dodatkowa klasa pomocnicza “scoped lock”, która zablokuje mutex w konstruktorze i odblokuje w destruktorze, więc nawet w przypadku wystąpienia wyjątku, mutex zostanie odblokowany zapobiegając deadlockowi Semaphore Obiekt synchronizacji, który przechowuje licznik zablokowań (i opcjonalnie posiada ograniczenie na maksymalną liczbę zablokowań) Licznik zablokowań zwiększa się za pomocą operacji signal (podniesienie semafora) Licznik zmniejsza się operacją wait; oczekiwanie na semaforze z zerowym licznikiem będzie blokować tak długo, aż semafor zostanie podniesiony Semafor nie ma pojęcia właściciela – każdy wątek może czekać lub podnosić semafor Mutex jest specjalnym przypadkiem semafora z maksymalną liczbą zablokowań 1 i ograniczeniem, że tylko wątek który zajął semafor (operacja wait zakończyła się sukcesem) może go zwolnić (podnieść, zasygnalizować) Semafory są trudniejsze do zrozumienia i poprawnego używania niż mutexy, więc raczej należy ich unikać, chyba że się wie co się robi ;) Condition variable Podstawowy składnik monitor-a Wątki będą czekać na obiekcie CV dopóki jeden z nich nie zostanie „wypuszczony” za pomocą operacji signal (lub wszystkie – za pomocą broadcast) CV musi być użyty razem z mutexem – wątek, którego oczekiwanie zakończy się sukcesem, automatycznie uzyskuje własność mutexa (atomowo) Condition variable wzorzec użycia Condition variable wzorzec Monitor Sposób użycia CV razem z mutexem z poprzedniego przykładu to wzorzec Monitor Jest to podstawowy schemat postępowania w przypadku gdy wątek oczekuje na jakieś zdarzenie wewnątrz sekcji krytycznej, zaś zdarzenie będzie sygnalizowane z innego wątka Jest to jedyny dostępny konstrukt synchronizacji/oczekiwania w Javie Jest to bezpieczny i sprawdzony sposób na uniknięcie wyścigów podczas oczekiwania na zdarzenie, ponieważ operacja oczekiwania i zwalniania mutexu oraz budzenia i blokowania mutexu odbywają się atomowo Barrier Barrier (lub rendezvous point) jest to miejsce w kodzie, gdzie grupa wątków jest blokowana i nie może kontynuować dopóki wszystkie wątki w grupie nie osiągną bariery Bariera wymusza synchronizację wątków – jest to użyteczne w wysokowydajnych obliczeniach i mieleniu numerków Read/Write lock Obiekt nazywany również shared mutex Specjalny rodzaj mutexa, który pozwala wielu wątkom równocześnie wykonać operację read ale tylko jeden wątek może mieć dostęp write Możliwy wzrost wydajności w przypadku, kiedy operacja write zdarza się rzadko, a operacje read często Natywne wsparcie w POSIXach, na Windows brak Jak działa? Zablokowanie dla zapisu zablokuje wszystkie inne wątki, zarówno czytające, jak i piszące Wątki zapisujące będą czekać aż wszystkie wątki piszące i czytające opuszczą RWLock Operacje specjalne: upgrade rwlock z trybu czytania do trybu zapisu Dużo wątków czytających może zagłodzić wątki piszące nigdy nie umożliwając im wejść w rwlock Events (zdarzenia) Windows Specjalny przypadek binarnego semafora (licznik 0/1), który w przeciwieństwie do mutexa nie uznaje własności 2 rodzaje: Manual reset – kiedy wątek zakończy sukcesem oczekiwanie na zdarzeniu, musi zostać ręcznie zresetowany do stanu nie-sygnalizowanego (pozostaje podniesiony dopóki nie zostanie jawnie opuszczony) Automatic reset – kiedy wątek zakończy sukcesem oczekiwanie na zdarzeniu, obiekt jest automatycznie, atomowo resetowany do stanu niesygnalizowanego (semafor opuszczony) Specyficzne i trudne w poprawnym użyciu – lepiej stosować „standardowe” obiekty (monitor)