Wątki
Wątek - definicja Ciąg instrukcji (podprogram) który może być wykonywane współbieżnie (równolegle) z innymi programami, Wątki działają w ramach tego samego procesu Współdzielą dane (mogą operować na tych samych danych) -> programowanie rozproszone ze współdzieleniem pamięci Obsługą wątków zajmuje się dany proces a nie system (obsługą procesów zajmuje się system) Przykład Wykorzystanie kilku rdzeni procesora jednocześnie Zrównoleglenie obliczeń Jednoczesna obsługa ekranu i procesu obliczeniowego
Implementacja wątków w Javie Wątki wymagają menadżera wątków procedury przydzielającej i dbającej o kolejność wykonywania wątków. W Javie jest to zaimplementowane wewnątrz JVM Możliwe są dwa sposoby tworzenia programów wielowątkowych Poprzez dziedziczenie po klasie Thread Poprzez implementację interfejsu Runnable
Dziedziczenie po klasie Thread Musimy wówczas przeciążyć metodę run: Uruchomienie osobnego wątku:
Implementacja Runnable Musimy wówczas zdefiniować metodę run: Uruchomienie osobnego wątku:
Metody klasy Thread Uruchamianie / Zatrzymywanie Start uruchomienie wątku Stop zatrzymanie wątku Run program wykonywany w osobnym wątku
Metody klasy Thread Uruchamianie / Zatrzymywanie Start uruchomienie wątku Stop zatrzymanie wątku Run program wykonywany w osobnym wątku Teoretycznie też działa ale tak nie wolno bo nie mamy współbieżności. Całość wykonywana jest sekwencyjnie
Ważne metody związane ze współbieżnością sleep(long ms) uśpienie wątku na określony czas wait() wprowadzenie wątku w stan oczekiwania notify()/notifyall() wzbudzenie wątku/wątków ze stanu oczekiwania Metody wait() i notify() (notifyall()) muszą być wywoływane w bloku/metodzie synchronized na tym samym obiekcie. void join([long milsec [, int nanosec]]) oczekiwanie na zakończenie wątku void setdaemon(boolean on) ustawienie wątku jako demona boolean isdaemon() czy wątek jest demonem boolean isalive() sprawdzenie czy wątek działa void interrupt(); - przerwanie działania wątku
Priorytety wątków Wątki mogą posiadać różne priorytety (ważności wykonywania) Zasada działania priorytetów: dopiero jak obsłużono wątki ważne obsługujemy wątki mniej ważne void setpriority(int priority) ustawianie priorytetu wątku, int getpriority() odczytanie priorytetu wątku. yield() oddanie czasu innym wątkom o tym samym priorytecie Stałe zdefiniowane w klasie w klasie Thread (typu final): Thread.MIN_PIORITY najniższy priorytet Thread.MAX_PIORITY maksymalny priorytet Thread.NORM_PIORITY normalny priorytet
Identyfikacja wątków String getname() pobranie nazwy wątku void setname(string name) ustawienie nazwy wątku long getid() zwrócenie identyfikatora wątku (unikatowa wartość)
Jeszcze kilka metod związanych z wątkami isalive() sprawdzenie czy wątek żyje setname(string name) nadanie wątkowi nazwy (pozwala na jego identyfikację) String getname() zwraca nazwę wątku
Wątek Demon Wątki Deamony zostają automatycznie zamknięte gdy główny program się kończy. Normalnie aplikacja działa tak długo jak działa najdłuższy wątek, i po jego zakończeniu JVM się kończy. W przypadku Deamonów tak nie jest! Metoda setdeamon(boolean) powinna być wywołana przed uruchomieniem wątku Uwaga na Deamony, bo ich zamknięcie nie powoduje wywołanie sekcji finaly co może powodować że strumienie nie zostaną poprawnie pozamykane
Wątki - problemy Często wątki korzystają ze wspólnych danych problem bezpieczeństwa danych Jednorodność danych (podczas odczytu dane nie mogą ulegać zmianie) Przykład: podczas odczytu danych przez jeden wątek drugi w międzyczasie dokonał ich modyfikacji. Współdzielone dane należy chronić przez tzw zakładanie zamka (blokowanie dojścia do danego obiektu/bloku instrukcji)
Sekcja krytyczne Sekcja krytyczna - w programowaniu współbieżnym fragment kodu programu, w którym korzysta się z zasobu dzielonego, a co za tym idzie w danej chwili może być wykorzystywany przez co najwyżej jeden wątek. i = 0 Współdzielony obszar pamięci Wątek A Operacja na i Wątek A Wątek B Operacja na i Wątek B czas
Sekcja krytyczne Sekcja krytyczna - w programowaniu współbieżnym fragment kodu programu, w którym korzysta się z zasobu dzielonego, a co za tym idzie w danej chwili może być wykorzystywany przez co najwyżej jeden wątek. i = 0 Współdzielony obszar pamięci Wątek A Operacja na i W. A Operacja na i Wątek A Wątek B Operacja na i W. B Operacja na i czas
Wątki problemy Co jeśli sekcja krytyczna jest niepoprawnie zaimplementowana
Przykład Zmienna statyczna np. licznik Inkrementujemy licznik Czekamy chwilę Tworzymy obiekt wątku 1 Tworzymy obiekt wątku 2 Czekamy aż skończy wątek 1 Uruchamiamy wątki Czekamy aż skończy wątek 2 Jaki wynik?
Przykład Zmienna statyczna np. licznik Inkrementujemy licznik Czekamy chwilę Tworzymy obiekt wątku 1 Tworzymy obiekt wątku 2 Czekamy aż skończy wątek 1 Uruchamiamy wątki Czekamy aż skończy wątek 2 Jaki wynik?
Dlaczego to nie działa Każdy wątek ma swój własny lokalny obszar pamięci, zmienne współdzielone domyślnie przechowywane są w postaci lokalnej kopi osobno dla każdego wątku Zmienna static liczbik Współdzielona między wątkami Lokalna kopia licznik Lokalna kopia licznik Wątek 1 Wątek 2
Volatile Volatile oznacza zmienną we wspólnym współdzielonym obszarze pamięci Zmienna static volatile liczbik Współdzielona między wątkami Wspólna kopia licznik Wspólna kopia licznik Wątek 1 Wątek 2
Volatile Volatile oznacza zmienną we wspólnym współdzielonym obszarze pamięci Czy teraz działa? Zmienna static volatile liczbik Współdzielona między wątkami Wspólna kopia licznik Wspólna kopia licznik Wątek 1 Wątek 2
Volatile Volatile oznacza zmienną we wspólnym współdzielonym obszarze pamięci Czy teraz działa? Zmienna static volatile liczbik Współdzielona między wątkami Wspólna kopia licznik Wspólna kopia licznik Wątek 1 Wątek 2
Problem!!! W1 odczytuje 0 W2 odczytuje 0 Wątek 1 Wątek 2 For ( ) End Pobierz licznik For ( ) Pobierz licznik Dodaj licznik + 1 Dodaj licznik + 1 Zapisz licznik Tak być powinno End Zapisz licznik licznik = 0 For ( ) Wątek 1 Wątek 2 End Pobierz licznik Dodaj licznik + 1 Tak może być For ( ) Pobierz licznik Zapisz licznik Dodaj licznik + 1 End W1 i 0+1 = 1 W2 i 0+1 = 1 Zapisz licznik W1 zapisuje 1 W2 zapisuje 1 Powinno być 2
Zmienne Atomic Zmienne typu ATOMIC to proste zmienne gwarantujące poprawną współbieżność Klasa AtomicBoolean AtomicInteger AtomicLong AtomicReference ( )
Volatile Jak używać Volatile aby było dobrze: Dla wielodostępu używać tylko do operacji atomowych Np. przypisanie / odczyt Voletile informuje JVM że operacje mają być prowadzone na współdzielonej pamięci JVM utrzymuje oryginalną kolejność operacji na zmiennej volatile (normalnie w wyniku optymalizacja kolejność operacji często jest zmieniana tak aby np..zminimalizować ilość wymiany danych pomiędzy rejestrami a pamięcią)
Bezpieczeństwo wątków zamki/monitory Zakładanie zamków na obiekcie: Wykorzystanie metody typu synchronized Wykorzystanie bloku typu sychronized np.: private double stankonta; public synchronized void wplata(double ile){ stankonta += ile; } public synchronized void wyplata(double ile){ stankonta -= ile; } Zasada działania podczas uruchomienia metody synchronized następuje zatrzaśnięcie blokady (na czas realizacji tej metody) Zakończenie wykonywania metody zwalnia blokadę inne wątki też mogą korzystać z danego obiektu.
Bezpieczeństwo wątków - zamki Sekcja krytyczna
Bezpieczeństwo wątków - zamki Sekcja krytyczna Każdy obiekt ma wewnętrzną zmienną typu boolean określającą zamek. Wywołanie synchronized zatrzaskuje zamek Gdy zamek jest zatrzaśnięty inne wątki nie mają dostępu do zmiennej przechodzą w stan wait Wyjście z metody synchronized zwalnia zamek Inni mogą wejść
Jak działa synchronized Algorytm Petersona dla 2 wątków Obydwa wątki uruchamiane są równolegle procedure processone; begin while true do begin {część prgramu przed sekcją krytyczną} flag[1]:=true; turn:=2; repeat whose:=turn; other:=flag[2]; until (whose=1 or not other) {sekcja krytyczna} flag[1]:=false; {pozostała część programu} end; end; procedure processtwo; begin while true do begin {część prgramu przed sekcją krytyczną} flag[2]:=true; turn:=1; repeat whose:=turn; other:=flag[1]; until (whose=1 or not other) {sekcja krytyczna} flag[2]:=false; {pozostała część programu} end; end; Źródło: http://www.algorytm.org/wzajemne-wykluczanie/algorytm-petersona-dla-2-procesow.html
Opis problemu: Wątki przykład Producent - Konsument Typowy przykład programowania współbieżnego Producent produkuje towar, po jego wyprodukowaniu umieszcza go w magazynie Jeśli produkt jest w magazynie, producent nie może do niego włożyć nowego towaru. Może włożyć dopiero jak konsument opróżni magazyn Konsument czeka, aż pojawi się towar w magazynie Gdy jest towar w magazynie konsument pobiera go i czeka na nowy towar Interpretacja przesyłanie komunikatów między dwoma wątkami
Wątki przykład Producent - Konsument Wait zwalnia zatrzask związany z synchronized Wait zwalnia zatrzask związany z synchronized
Wątki przykład Producent - Konsument Wait zwalnia zatrzask związany z synchronized
Wątki przykład Producent - Konsument
Wątki przykład Producent - Konsument Wait zwalnia zatrzask związany z synchronized Wait zwalnia zatrzask związany z synchronized
Semafory Ograniczenia synchronized Działają jedynie gdy jeden wątek ma dostęp do sekcji krytycznej, czasem chcemy by jednocześnie kilka wątków miało dostęp do danego zasobu, wówczas synchronized nie pomoże Rozwiązanie: Semaphore Semafor pozwala na wejście do sekcji krytycznej określonej liczbie wątków: Wejście i wyjście odbywa się na zasadzie uzyskiwania permitów liczba możliwych permitów jest ograniczona przez użytkownika w konstruktorze.
Klasa Semaphore Konstruktor definiujemy liczbę permitów i/lub czy semafor ma być fair. Bycie fair oznacza, że dany wątki które wystąpiły jako pierwsze po dostęp dostaną go jako pierwsze. Jeśli opcja nie zaznaczona to bycie nie fair Oznacza, że dostęp do zasobu jest losowy Pobranie jednego lub określonej liczby permitów. Jeśli permit niedostępny to acquire blokuje wątek aż permit będzie dostepny Zwraca liczbę int opisującą ilość dostępnych permitów Zwolnienie permitu/permitów wywoływane na przy wyjściu z sekcji krytycznej i powoduje oddanie do semafora permitów
Semaphore
Semaphore przykładowa implementacja
Przerywanie wątków Wątków nie należy przerywać przez wywołanie stop!!! Wątki przerywamy przez wywołanie polecenia interrupt() Polecenie interrupt() doprowadza do wystąpienia wyjątku w metodach uśpionych (sleep, wait) Nie da się normalnie przerwać wątku realizującego proces obliczeniowy
Przerywanie wątku
Przerywanie wątku
Przerywanie wątku
Przerywanie wątku
Przerywanie wątku Dlaczego wyjątek? Problem gdy w wątku otwarte zasoby Gdyby to przerywanie było niekontrolowane to problem ze zwalnianiem zasobu Wyjątek pozwala też na kontrolowane zamkniecie wątku Wyjątek pozwala na posprzątanie
Bezpieczeństwo wątków zamki/monitory Słowo synchronized użyte przy deklaracji funkcji zakłada zamek na obiekcie this Synchronized można użyć jako instrukcji języka np: Zamek zamykamy zawsze na jak najkrótszym kawałku kodu
Bezpieczeństwo wątków zamki/monitory
Bezpieczeństwo wątków zamki/monitory
Problem Wspólna synchronizacja wielu wątków implementowanych w różnych klasach Zamek można zakładać na dowolnym obiekcie nie koniecznie this np.
Problem ucztujących filozofów Przy wspólnym stole zasiadło pięciu chińskich filozofów. Filozofowie z zamiłowaniem oddają się czynności myślenia, jednakże muszą się co jakiś czas posilać. Na stole, przy którym siedzą, znajduje się pięć talerzy (po jednym dla każdego), jeden półmisek z ryżem (wspólny dla wszystkich, do którego dostęp jest nieograniczony) oraz pięć pałeczek, niezbędnych do jedzenia ryżu, które zostały ułożone na przemian z talerzami, tak że każdy filozof może mieć dostęp do dwóch pałeczek, po prawej oraz po lewej stronie talerza, przy czym każdą pałeczkę współdzieli z sąsiadem. Zdefiniuj wątek Filozof, zapewniając następujące warunki: a. Każdy filozof może myśleć do woli, niezależnie od pozostałych. b. Filozof może zacząć jeść tylko jeśli udało mu się podnieść dwie pałeczki. c. Pałeczka nie może być podniesiona przez dwóch filozofów w tym samym czasie. d. Pałeczki podnoszone są pojedynczo. e. Program powinien działać zarówno w przypadku ostrej rywalizacji o jedzenie, jak i w przypadku braku rywalizacji. f. Nie może dochodzić do zakleszczeń (zarówno w wersji deadlock jak i livelock) ani do zagłodzenia (w tym przypadku rozumianego dosłownie). Źródło: materiały dydaktyczne do szkolenia Komputery Dużej Mocy UMCS 2012
Rozwiązanie 1
Rozwiązanie 1 Może dojść do zagłodzenia filozofów
Rozwiązanie 1 Rozwiązanie: umożliwić podejście do stołu max tylu filozofom aby nie doszło do zagłodzenia, tak aby przynajmniej jeden miał szansę podnieść 2 pałeczki Co jeśli na raz każdy podniesie jedną pałeczkę?
Rozwiązanie 2 Pozwalamy wejść tylu filozofom aby zawsze jeden na pewno mógł podjąć parę pałeczek Jak obydwie pałeczki oddane to zwalniamy miejsce przy stole
Problem ucztujących filozofów Jaki scenariusz problem opisuje Gdy mamy konkurencyjny dostęp do kilku zasobów naraz Konieczność zarządzania wspólnym dostępem do zasobów Unikanie deadlocków poprzez zagwarantowanie aby przynajmniej jeden zasób miał szansę na kompletny dostęp do wszystkich wymaganych zasobów
Problem czytelników i pisarzy Klasyczny problem programowania wielowątkowego: Mamy jeden zasób dzielony wśród użytkowników odpowiednio czytelników i pisarzy Z zasobu może korzystać na raz wielu czytelników Z zasobu może korzystać na raz jeden pisarz gdy pisarz pisze do zasobu nikt nie może czytać,
Potrzebny arbiter Arbiter zarządza kto ma dostęp do zasobu Czy czytelnicy Czy pisarze Jakie są priorytety czytelników/pisarzy
Czytelnicy i pisarze
Czytelnik
Pisarz
Czytelnia
Czytelnia Może dojść do zagłodzenia pisarza, bo może się zdarzyć tak, że zawsze będzie jakiś czytelnik
Czytelnia v2 Zbieramy info o tym że pisarz czeka w kolejce Jeśli pisarz czeka to odstępujemy mu miejsca Jak pisarz się doczekał to zwalniamy czas
Czytelnia v2 Jeśli pisarz czeka to odstępujemy mu miejsca Zbieramy info o tym że pisarz czeka w kolejce Może dojść do zagłodzenia czytelników gdyby było więcej pisarzy, bo oni zawsze mają pierwszeństwo. Jak pisarz się doczekał to zwalniamy czas
Czytelnia v3
Czytelnia v3 Zbieramy informację o tym że pisarz skończył pisać Jeżeli pisarz czeka to mimo wszystko oddajemy czas czytelnikom aby zdążyli to przeczytać Na koniec pisania ustawiamy info że pisarz coś napisał Resetujemy gdy przynajmniej jeden czytelnik wszedł
Java Collections a wątkobezpieczność Dwie struktury są wątko-bezpieczne: Vector - odpowiednik listy Hashtable odpowiednik HashMap Wątkobezpieczność można osiągnąć przez Collections.synchronizedXXX(collection) Poprzez wykorzystanie struktur z pakietu java.util.concurrent
Inne elementy Java Collections Fabryka: Collections.synchronizedXXX(collection) Gdzie XXX to: Collection, List, Map, Set, SortedMap oraz SortedSet Pozwala na tworzenie wątko-bezpiecznych kolekcji Uwaga 1!!! wątko-bezpieczne operacje to: dodawanie, usuwanie, itp.. Uwaga 2!!! Iteratory nie są bezpieczne
Inne elementy Java Collections + iteratory Scenariusz:
Inne elementy Java Collections + iteratory Scenariusz: Wyjątek: java.util.concurrentmodificationex ception Bo w trakcie działania wątku obsługującego iterator dodajemy nowy element i iterator nie może zostać zmodyfikowany!!!!!
Inne elementy Java Collections + iteratory Rozwiązanie: Operacje na iteratorach stanowią sekcję krytyczną Uwaga: Jak iterujemy to inne wątki są blokowane!!!! Bardzo niekorzystne
Inne elementy Java Collections Concurrent java.util.concurrent cz.1 Copy on write collection dane przechowywane są w niemodyfikowalnej tablicy. Dodanie nowego elementu powoduje stworzenie nowej tablicy dobra gdy ważniejszy jest odczyt niż zapis danych do kolekcji Przykłady: CopyOnWriteArrayList, CopyOnWriteArraySet.
Inne elementy Java Collections Concurrent java.util.concurrent cz.2 Compare-And-Swap (CAS) collection modyfikacja wartości odbywa się poprzez zrobienie lokalnej kopii wartości i wówczas przeprowadzana jest operacja. Gdy na koniec uzyskujemy wynik operacji, wartość jest porównywana z wartością początkową. Gdy obie są takie same to zapisujemy wynik, gdy nie to powtarzamy obliczenia jeszcze raz. Przykłady: ConcurrentLinkedQueue, ConcurrentSkipListMap.
Inne elementy Java Collections Concurrent java.util.concurrent cz.3 Wykorzystanie semaforów (java.util.concurrent.lock.lock) W odróżnieniu od synchronized, Lock zwykle zakładane są na fragmencie kolekcji, więc blokują jedynie jej fragment a nie całość. Przykłady większość kolejek implementujących BlockingQueue ConcurrentHashMap - ConcurrentMap (interfejs)
Egzekutory Java oferuje egzekutory: interface Executor{ void execute(runable task) } pozwala na elastyczne zarządzanie wątkami.
Egzekutory Java oferuje dla Egzekutorów również: ExecutorService Interface Rozszerza Executor i pozwala na zarządzanie zadaniami jak również zwraca Feature obiekty do śledzenia progresu w wykonywaniu asynchronicznych zadań. Collable<T> - alternatywa dla Runnable pozwala na zwracanie wyniku z metody run. Metody: get() zwraca wynik i czeka na zakończenie zadania cancel() skasowanie zadania isdone() czy zadanie zakończone iscancelled() czy zadanie skasowane
ExecutorService Przykład ThreadPoolExecutor( int corepoolsize, //Bazowa liczba watków int maximumpoolsize, //Maksymalna liczba wątków long keepalivetime, //Czas utrzymania wątków TimeUnit unit, //jednostka czasu dla keepalive BlockingQueue<Runnable> workqueue //Kolejka zadań )
ExecutorService przykładowe implementacje Klasa Executors zawiera przykładowe implementacje ThreadPoolExecutor: newsinglethreadexecutor(threadfactory threadfactory) tworzy jeden wątek roboczy do wykonywania zadań zgodnego z kolejkom newcachedthreadpool() tworzy nowe wątki o ile sa konieczne, jeśli nie to używa starych newfixedthreadpool(int nthreads) pula wątków o stałym rozmiarze newscheduledthreadpool(int corepoolsize) pozwala na zarządzaniem kolejnoscią wykonania np. wykonanie okresowe zadań
Przykład Calleble będzie zwracał String Calleble zwraca String Zapamietujemy Feature Tworzymy egzekutor na 10 wątków Dodajemy zadania do wykonania Odczytujemy wyniki w sposób asynchroniczny Przeglądając które zadanie się skończyło Wyłączamy egzekutor
Przykład 2 (bardziej elegancki) Calleble będzie zwracał String Calleble zwraca String Tworzymy CompletionService Tworzymy egzekutor na 10 wątków Dodajemy zadania do wykonania Wyłączamy egzekutor Polecenie take() w sposób blokujący czeka aż pojawi się jakiś wynik.
DeadLocks Występuje gdy blokady składane sa w niewłaściwej kolejności Wątek 1 blokuje A, czeka na B Wątek 2 blokuje B, czeka na A
DeadLocks Scenariusz: System wielowątkowy który z obiektów buduje drzewo. Może dojść do sytuacji w której: Wątek A dodaje dziecko B Wątek B dodaje obiekt rodzica A A Wywołuje metodę addchild(b) B Wywołuje metodę addparent(a)
A B Żródło: http://tutorials.jenkov.com/java-concurrency/deadlock.html
DeadLocks Występuje gdy blokady składane sa w niewłaściwej kolejności Wątek 1 blokuje A, czeka na B Wątek 2 blokuje B, czeka na A Rozwiązanie: Zakładanie blokad zawsze w tej samej kolejności np. najpierw zakładamy zawsze na rodzicu potem na dziecku modyfikacja kolejności zakładania blokad setparent oraz setparentonly
NestedLock Występuje gdy mamy zagnieżdżone monitory Wywołanie wait() na jednym nie zwalnia blokady na drugim monitorze Wątek 1 Wątek 2
blokada na A, blokada na B Wait() na B (odblokowanie B) Ale A pozostaje ciągle zablokowane Drugi wątek nigdy nie osiągnie polecenia notify(), gdyż jest zablokowany na this Żródło: http://tutorials.jenkov.com/java-concurrency/nested-monitor-lockout.html
NestedLock Występuje gdy mamy zagnieżdżone monitory Wywołanie wait() na jednym nie zwalnia blokady na drugim monitorze Rozwiązanie: Unikanie zagnieżdżonych monitorów