PRS04.rst

Przetwarzanie równoległe i strumieniowe

Uwaga! Kod z którego będziemy korzystać na zajęciach jest dostępny na branchu DeadlockClassesStart w repozytorium https://github.com/WitMar/PRS2020 . Kod końcowy można znaleźć na branchu DeadlockClassesEnd.

Jeżeli nie widzisz odpowiednich gałęzi na GitHubie wykonaj Ctr+T, a jak to nie pomoże to wybierz z menu Git->Fetch.

Dodatkowe narzędzia synchronizacji

Volatile

Zmienne w programie javowym zapisywane są w pamięci RAM. Gdy procesor chce odczytać zmienną pobiera ją z pamięci RAM i dla często pobieranych zmiennych może ją umieścić w cachu. Jeżeli mamy wiele procesorów w naszym komputerze może to spowodować, że w zadanym czasie różne procesory będą widziały różne wartości zmiennych. Żeby wymusić by każdy procesor pobierał zawsze daną zmienną z pamięci RAM należy użyć słowa volatile.

colss

private volatile boolean terminated;

Volatile jest zwykle używany do flag, które są modyfikowane przez jeden wątek i odczytywane przez inny (jak wartość logiczna w powyższym fragmencie). Globalnie zmiennej volatile użyjemy, jeśli do zmiennej można uzyskać dostęp przez kilka wątków i nie wykonują one na niej sekwencji operacji w sposób atomowy (na przykład nie inkrementują jej wartości). W przypadku zmiennych zliczających itp. powinieneś rozważyć użycie mechanizmu synchronizacji. W naszym przypadku będziemy z niej korzystać tylko do kończenia wykonywania wątków.

Spójrz na klasę Volatile.java żeby zobaczyć przykład.

Priorytety wątków

W Javie priorytet wątku jest określany przez liczbę całkowitą z zakresu od 1 do 10. Im większa liczba całkowita, tym wyższy priorytet. Harmonogram wątków używa tej liczby całkowitej, aby określić, który z nich powinien zostać wykonany. Klasa Thread definiuje trzy typy priorytetów:

Minimalny priorytet
Normalny priorytet
Maksymalny priorytet

Klasa Thread definiuje te typy priorytetów jako stałe MIN_PRIORITY, NORM_PRIORITY i MAX_PRIORITY, odpowiednio o wartościach 1, 5 i 10. NORM_PRIORITY to domyślny priorytet dla nowego wątku.

Thread thread1 = new Thread(() -> run(1));
thread1.setPriority(10);

Przykład zastosowania zostanie pokazany w sekcji zagłodzenie.

TryLock()

Metody interfejsu Locks:

void lock() – uzyskaj blokadę, jeśli jest dostępna; jeśli blokada nie jest dostępna, wątek zostanie zablokowany do momentu zwolnienia blokady
boolean tryLock() – jest to nieblokująca wersja metody lock(); próbuje natychmiast uzyskać blokadę, zwraca wartość true, jeśli blokowanie się powiedzie
boolean tryLock(long timeout, TimeUnit timeUnit) – jest to podobne do tryLock(), z tą różnicą, że czeka przez określony czas zanim zrezygnuje z próby uzyskania blokady

Typowe użycie tryLock():

    Lock lock = ...;
if (lock.tryLock()) {
  try {
      // manipulate protected state
  } finally {
      lock.unlock();
  }
} else {
      // perform alternative actions
}

Przykładem przypadku wykorzystania tryLock jest wątek przetwarzający zbiór elementów, czasami próbujący zatwierdzić przetworzone elementy (np poprzez zapis do bazy danych). Jeśli uzyskanie blokady dla zapisu nie powiedzie się, elementy zostaną zatwierdzone w następnej udanej próbie lub podczas ostatniego, obowiązkowego zatwierdzenia. Wtedy dostęp do zapisu nie blokuje nam całego przetwarzania.

Spójrz na klasę TryLock.java żeby zobaczyć przykład.

Problemy przetwarzania równoległego

Race condition

Race condition występuje, gdy co najmniej dwa wątki mogą uzyskać dostęp do tych samych danych i jednocześnie próbują je zmienić. Ponieważ algorytm planowania wątków może przełączać się między wątkami w dowolnym momencie, nie znasz kolejności, w jakiej wątki będą próbowały uzyskać dostęp do udostępnionych danych. Dlatego wynik zmiany danych jest zależny od algorytmu planowania wątków, tj. oba wątki „ścigają się” w celu uzyskania dostępu/zmiany danych.

Programiści mogą zapobiegać race condition w systemach operacyjnych i innym oprogramowaniu na dwa sposoby:

* Przez unikanie dzielenia zasobów. Oznacza to przeglądanie kodu w celu upewnienia się, że gdy współużytkowane zasoby są częścią systemu lub procesu, istnieją niepodzielne operacje, które działają niezależnie od innych procesów. Można również używać obiektów atomowych, lub niezmienialnych (immutable - których nie można zmienić po utworzeniu).
* Przez synchronizacje wątków. Tutaj dana część programu może wykonywać tylko jeden wątek na raz:
// Uzyskaj blokadę dla x
if (x == 5)
{
        y = x * 2; // Teraz nic nie może zmienić x, dopóki blokada nie zostanie zwolniona.
// Dlatego y = 10
}
// zwolnij blokadę dla x

Klasycznym przykładem na race condition jest licznik z klasy Counter.java z poprzednich zajęć.

W programowaniu dwie główne sytuacje race condition występują gdy wiele wątków próbuje odczytać zmienną, a następnie każdy z nich zapisuje zmiany, może wystąpić jedna z następujących sytuacji:

Odczyt-modyfikacja-zapis. Ten rodzaj wyścigu ma miejsce, gdy dwa procesy odczytują wartość w programie i zapisują nową wartość. Oczekuje się, że te dwa procesy zajdą sekwencyjnie — pierwszy proces zapisze swoją wartość, a następnie drugi proces odczyta tę wartość i zapisze inną.

Na przykład, jeśli czeki z konta czekowego są przetwarzane sekwencyjnie, system upewni się, że na koncie jest wystarczająca ilość środków do przetworzenia czeku A, a następnie ponownie sprawdzi, czy po przetworzeniu czeku A jest wystarczająca ilość środków do przetworzenia czeku B. Jeśli jednak oba czeki są przetwarzane w tym samym czasie, system może odczytać tę samą wartość salda konta dla obu procesów i podać nieprawidłową wartość salda konta, powodując przekroczenie salda konta.

Przykład:

rrsssds

Drugi typ:

Sprawdź, a następnie działaj. Ten stan wyścigu ma miejsce, gdy dwa procesy sprawdzają wartość, na której każdy z nich podejmie działanie zewnętrzne. Oba procesy sprawdzają wartość, ale tylko jeden proces powinien pobrać wartość ze sobą.

Przykładem jest np wielokrotne wzbudzenie alarmu na podstawie jednego odczytu z sensora.

Wykrywanie i identyfikowanie warunków race condition jest trudne. Programiści używają dynamicznych i statycznych narzędzi analitycznych do identyfikacji warunków wyścigu. Narzędzia do testowania statycznego skanują program bez jego uruchamiania. Ich wadą jest wolne działanie i produkcja fałszywych raportów. Narzędzia do analizy dynamicznej mają mniej fałszywych raportów, ale mogą nie wykrywać wszystkich race condition.

Zadanie 1

Popraw przykład z ticketBooking.java tak, żeby nie było problemu overbookingu.

Zakleszczenie (deadlock)

Zakleszczenie występuje, gdy co najmniej dwa wątki czekają w nieskończoność na blokadę lub zasób utrzymywany przez inny z wątków. W związku z tym aplikacja może się zatrzymać lub zakończyć niepowodzeniem, ponieważ zakleszczenie wątków nie może być kontynuowane.

col

Zakleszczenie może się pojawiać gdy wątek do działania potrzebuje uzyskać dostęp do więcj niż jednej blokady (locka).

Jak przeciwdziałać zakleszczeniu:

* używając interfejsu tryLock() na obiekcie Lock, który próbuje pobrać blokadę a w przypadku niepowodzenia po jakimś czasie przechodzi dalej
* implementując pobieranie blokad w taki sposób, by każdy z wątków pobierał je w tej samej kolejności

Spójrz na klasę Deadlock.java żeby zobaczyć przykład.

Zagłodzenie (starvation)

O zagłodzeniu mówimy, gdy określony wątek nie uzyskuje dostępu do obiektu lub zasobu, co prowadzi do wydłużenia czasu oczekiwania i wykonania. Dzieje się tak, gdy współdzielone zasoby są niedostępne przez długi czas przez „chciwe” wątki. Załóżmy na przykład, że obiekt udostępnia zsynchronizowaną metodę, której zwrócenie często zajmuje dużo czasu. Jeśli jeden wątek często wywołuje tę metodę, inne wątki, które również wymagają częstego zsynchronizowanego dostępu do tego samego obiektu, będą mogą być przez niego blokowane.

s

Przyczyn zagłodzenia wątków w javie jest wiele, niektóre z nich opisano poniżej:

Działający wątek o wysokim priorytecie: Może zaistnieć przypadek, w którym wątek o wysokim priorytecie, zajmuje CPU i wymaga intensywnego przetwarzania, które wymaga dużo czasu na ukończenie, więc aby ta praca została całkowicie wykonana, inne wątki o niskim priorytecie muszą czekać przez długi czas, co prowadzi do zagłodzenia.
Zsynchronizowany blok w Javie: Może zaistnieć przypadek, w którym kolejność, w której wątki mogą wejść do zsynchronizowanego bloku, ma ustaloną kolejność, co powoduje oczekiwanie na zasoby i obiekty przez inny wątek prowadzący do zagłodzenia.
Wątki oczekujące na obiekt pozostaje w nieskończoność: metoda notify() w java nie śledzi w wątkach tego, który konkretny wątek jest wybudzany, jeśli istnieje wiele wątków, może istnieć ryzyko, że którykolwiek z wątków jest przetwarzany, a inne wątki nigdy nie są wywoływane do wykonania.

Spójrz na klasę Starvation.java oraz StarvationWaitNotify.java żeby zobaczyć przykład. Uwaga! Przykłady w teorii pokazują scenariusze możliwego załodzenia jednak nie zawsze uruchomienie da nam spodziewane rezultaty w zależności od tego ile procesorów przyzna nam system operacyjny i jak szereguje wątki.

Livelock

Wątek często działa w odpowiedzi na działanie innego wątku. Jeśli akcja jednego wątku jest również odpowiedzią na akcję innego wątku, może wystąpić livelock. Podobnie jak w przypadku zagłodzenia, wątki z blokadą na żywo nie są w stanie poczynić dalszych postępów. Jednak wątki nie są blokowane — są po prostu zbyt zajęte odpowiadaniem sobie nawzajem, aby wznowić pracę. Jest to porównywalne z dwojgiem ludzi, którzy próbują się minąć na korytarzu: Alphonse przesuwa się w lewo, aby przepuścić Gastona, podczas gdy Gaston przesuwa się w prawo, aby przepuścić Alphonse'a. Widząc, że nadal się blokują, Alphone przesuwa się w prawo, a Gaston w lewo. Wciąż się blokują...

cold

Spójrz na klasę Livelock.java żeby zobaczyć przykład.

Livelocks można uniknąć, wykorzystując ReentrantLock jako sposób na określenie, który wątek czeka dłużej, aby można było przypisać mu blokadę. W ramach najlepszej praktyki jest nie blokowanie blokad; jeśli wątek nie może uzyskać blokady, powinien zwolnić wcześniej nabyte blokady, aby spróbować ponownie później.

Przykłady ograniczeń przetwarzania równoległego

* Ograniczenie: zadania przychodzą w określonej kolejności, podczas przetwarzania kolejność nie ma znaczenia - ale po zakończeniu przetwarzania kolejność zadań musi zostać przywrócona.
* Ograniczenia kolejnościowe, zadania danego typu powinny być zawsze wykonane w tej samej kolejności, w której przychodzą.
* Ograniczenia dostępu do zasobów (sekcja krytyczna), tylko jeden wątek może na raz korzystać np. z jakiegoś urządzenia itp.
* Ograniczenia na liczbę wątków których uruchomienie w systemie przyspiesza przetwarzanie danych. Dlatego nie możemy np. tworzyć nowego wątku dla każdego nowego zapytania / nowo przychodzących danych.
* Priorytet - niektóre zadania powinny być wykonywane z wyższym priorytetem niż inne

Zadanie 2

Dla klasy shop.java napisz metody pozwalające na przeprowadzenie zakupów dla trzech typów produktów GARNEK, SZKLANKA, TALERZ.

Zadanie 3

Dla klasy shop.java napisz metody pozwalające na przeprowadzenie zakupów dla trzech typów produktów zakładając, że do każdego zamówionego TALERZA dokładamy SZKLANKĘ gratis.

Zadanie 4

Dla klasy shop.java napisz metody pozwalające na przeprowadzenie zakupów dla trzech typów produktów zakładając, że do każdego zamówionego TALERZA dokładamy SZKLANKĘ gratis. Dodaj możliwość obsługi zwrotów do sklepu, tak by zwrot miał pierwszeństwo przed zakupem.

*

Wykorzystano materiały z: