WIELOWĄTKOWOŚĆ Waldemar Korłub Platformy Technologiczne KASK ETI Politechnika Gdańska
Wydajność 2 Do 2005 roku wydajność komputerów poprawiano zwiększając częstotliwość taktowania procesora 1995: Pentium Pro 200 MHz 2005: Pentium 4 3,8 GHz 2017: Core i7-7700k 4,2 GHz ~19-krotny wzrost częstotliwości taktowania Dalsze zwiększanie częstotliwości jest trudne ze względu na ilość emitowanego ciepła Po 2005 roku rozwój w zakresie efektywności energetycznej i wielordzeniowości 2017: Xeon E7-8894 v4 n 2,4 GHz (3,6 GHz z Turbo Boost) n 24 rdzenie fizyczne + Hyper-Threading 48 wątków
Wydajność 3 Obecnie podstawową metodą przyspieszania obliczeń jest ich zrównoleglenie Wiele wątków/procesów na CPU n Przetwarzanie rozproszone, platformy technologiczne Akceleratory obliczeniowe, karty graficzne n Przetwarzanie równoległe CUDA Wiele węzłów połączonych w ramach klastra n Systemy obliczeniowe wysokiej wydajności
Zrównoleglenie obliczeń na CPU 4 Wiele procesów Osobne instancje aplikacji Każda z osobną przestrzenią adresową Komunikacji między procesami (IPC): potoki, pliki, gniazda (ang. sockets), RPC, itd. Wielowątkowość Wiele wątków w obrębie jednego procesu Wspólna przestrzeń adresowa n Wymiana informacji między wątkami przez współdzielone zmienne konieczna synchronizacja dostępu
Zastosowania (tylko niektóre) 5 Szybsze przetwarzanie danych Eksport materiału wideo Uczenie głębokie Szybsza kompresja/dekompresja plików Aplikacje serwerowe Obsługa wielu klientów równocześnie Serwery HTTP Systemy transakcyjne
Wielowątkowość w Javie 6 Thread + Runnable Najbardziej niskopoziomowe API Sekcje krytyczne (synchronized) Kontrolują dostęp do współdzielonych obiektów java.util.concurrent.* ExecutorService ułatwia zarządzanie pulą wątków roboczych i zadaniami do wykonania Implementacje typowych wzorców dla programowania równoległego, np. blokady cykliczne, semafory, kolejki zadań Fork/Join Framework Dla problemów klasy dziel i rządź Parallel Stream Współbieżna obsługa strumieni
Dziedziczenie po klasie Thread niezalecane 7 Przygotowanie wątku przez dziedziczenie po klasie Thread Należy nadpisać metodę run() operacje do wykonania w osobnym wątku Metoda start() uruchamia nowy wątek Niezalecane n Wiąże zadanie do wykonania ze sposobem jego uruchomienia n Thread jest klasą dziedzicząc po klasie Thread nie można dziedziczyć po innych klasach
Thread + Runnable 8 Zadanie do wykonania definiowane jako obiekt implementujący interfejs Runnable Możliwość dziedziczenia po innych klasach Runnable jest interfejsem funkcyjnym (pojedyncza metoda run()) możliwość zastosowania wyrażenia lambda Obiekt implementujący Runnable należy przekazać jako argument konstruktora klasy Thread Thread.start() uruchamia wątek w tle
Thread + Runnable 9 private void longrunningjob(string path, int counter){ //...długotrwałe i intensywne //obliczeniowo operacje... } //przygotowanie zadania w innej metodzie: Runnable task = () -> this.longrunningjob("/home/stwrl/tmp", 42); //uruchomienie wątku: Thread thread = new Thread(task); thread.start();
Oczekiwanie na zakończenie zadania 10 Oczekiwanie na zakończenie zadania wywołanie blokujące: thread.join(); Sprawdzenie stanu zadania wywołanie nieblokujące: if (thread.isalive()) { //operacje ciągle trwają } else { //operacje się zakończyły }
Przerwanie wątku anulowanie zadania 11 Wątek można przerwać bez oczekiwania na zakończenie wykonywanego zadania: thread.interrupt(); Wątek otrzymujący przerwanie powinien zwolnić wykorzystywane zasoby (np. uchwyty plików, gniazda sieciowe) i zakończyć działania Skąd wątek wie, że otrzymał przerwanie? Wyjątek InterruptedException n O ile wątek jest w trakcie wykonywania metody, która może wyrzucić taki wyjątek Sprawdzenie przerwania metodą Thread.interrupted();
Przerwanie wątku 12 while (!Thread.interrupted()) { //przetwarzanie danych } try { //oczekiwanie przed kolejną iteracją Thread.sleep(1000); } catch (InterruptedException e){ break; } //zwolnienie zasobów //zakończenie zadania
Potrzeba synchronizacji 13 Pewne sekcje programu nie powinny być wykonywane przez wiele wątków równocześnie Integer tmp = this.counter; tmp = /*...złożone obliczenia...*/ tmp + 2; this.counter = tmp; Wątek nr 1 Wątek nr 2 tmp = 17 tmp = 17+2 = 19 counter = 19 tmp = 17 tmp = 17+2 = 19 counter = 19
Synchronizacja sekcja krytyczna 14 synchronized (this) { } Integer tmp = this.counter; tmp = /*...złożone obliczenia...*/ tmp + 2; this.counter = tmp; Wątek nr 1 Wątek nr 2 tmp = 17 tmp = 17+2 = 19 counter = 19 tmp = 19 tmp = 19+2 = 21 counter = 21 lub Wątek nr 1 Wątek nr 2 tmp = 17 tmp = 17+2 = 19 counter = 19 tmp = 19 tmp = 19+2 = 21 counter = 21
Synchronizacja sekcja krytyczna 15 Jeśli sekcja krytyczna ma obejmować całą metodą można użyć uproszczonej składni: public synchronized void calculate(){ Integer tmp = this.counter; tmp = tmp + /*...obliczenia...*/1; this.counter = tmp; }
16 Executor Service
Pule wątków 17 Przykład: aplikacja serwerowa Podejście #1: jeden wątek do obsługi wszystkich klientów Każdy kolejny klient oczekuje na zakończenie obsługi poprzedniego Podejście #2: uruchamiamy nowy wątek do obsługi każdego klienta Obsługa wielu klientów równocześnie 1000 klientów à 1000 wątków à zagłodzenie n Łatwość ataku typu Denial of Service Uruchomienie nowego wątku jest kosztowną operacją
Pule wątków 18 Podejście #3: pula wątków o określonym rozmiarze do obsługi klientów Obsługa wielu klientów równocześnie Liczba obsługiwanych równocześnie klientów dostosowana do możliwości serwera (np. liczby rdzeni CPU) n Uniknięcie zagładzania Jeśli klientów jest więcej niż wątków w puli, nadmiarowi klienci czekają na zwolnienie wątku roboczego Wątki w puli są uruchamiane przy starcie aplikacji n Uniknięcie kosztu wielokrotnego uruchamiania wątków w trakcie działania aplikacji n Zadania obsługi klientów uruchamiane w istniejących wątkach Rozmiar puli wątków może być dynamicznie dostosowywany do liczby klientów (obciążenia serwera)
Pule wątków 19 Na szczęście nie musimy implementować tego ręcznie Pula wątków o stałym rozmiarze: ExecutorService executor = Executors.newFixedThreadPool(4); Pula wątków o dynamicznym rozmiarze: ExecutorService dynamicexecutor = new ThreadPoolExecutor( 2, //minimalna liczba wątków 16, //maksymalna liczba wątków 60, //maksymalny czas nieaktywności wątku TimeUnit.SECONDS, new LinkedBlockingQueue<>() //kolejka zadań );
Pule wątków 20 Wysłanie zadania do puli wątków: executor.submit( () -> longrunningjob("/home/stwrl/tmp", 42)); Wysłanie zadania, które zwraca wartość: Future<Integer> future = executor.submit( () -> calculateresult("data.txt"));
Future<T> 21 Umożliwia sprawdzenie, czy zadanie zostało zakończone oraz pobranie jego wyniku get() oczekuje na zakończenie i zwraca wynik (wywołanie blokujące) isdone() sprawdza, czy zadanie zostało zakończone (nieblokujące) cancel() umożliwia anulowanie zadania
22 Fork/Join Framework
Fork/Join Framework 23 Wysokopoziomowe API dla zadań poddających się strategii dziel i rządź Zadanie definiowane jako klasa dziedzicząca po RecursiveTask Zadanie generuje rekurencyjnie nowe zadania o mniejszej złożoności etap fork Gdy zadania są wystarczająco małe następuje ich wykonanie Wyniki zadań są łączone w celu uzyskania końcowego rezultatu etap join
Sumowanie elementów tablicy: fork 24 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1
Sumowanie elementów tablicy: join 25 226 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 103 25 12 7 17 1 21 6 14 123 25 35 11 4 16 22 9 1 61 42 75 48 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 37 24 22 20 60 15 38 10 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1 25 12 7 17 1 21 6 14 25 35 11 4 16 22 9 1
class Sum extends RecursiveTask<Long> { static final int THRESHOLD = 5000; int low; int high; int[] array; } Sum(int[] array, int l, int h) { this.array = array; low = l; high = h; } protected Long compute() { if (high - low <= THRESHOLD) { long sum = 0; for (int i=low; i<high; ++i) { sum += array[i]; } return sum; } } int mid = low + (high - low) / 2; Sum left = new Sum(array, low, mid); Sum right = new Sum(array, mid, high); left.fork(); long rightans = right.compute(); long leftans = left.join(); join return leftans + rightans; zadanie jest wystarczająco małe fork 26 Fork/Join: RecursiveTask
Wywołanie RecursiveTask 27 Tablica liczb do zsumowania: int[] array = /*...*/; Z użyciem puli common: Long sum = ForkJoinPool.commonPool().invoke( new Sum(array, 0, array.length)); Z użyciem własnej puli wątków: ForkJoinPool pool = new ForkJoinPool(4); pool.invoke(new Sum(array, 0, array.length));
28 Pytania?