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)

Podobne dokumenty