1. Wielowątkowość vs Równoległość
Wielowątkowość: dużo, ale niekoniecznie jednocześnie
Wielowątkowość to sytuacja, gdy w twoim programie istnieje kilka wątków wykonania. Każdy wątek to jak samodzielna linia działań: jeden coś oblicza, drugi czeka na wejście użytkownika, trzeci zapisuje dane do pliku. W Javie tworzysz wątki przez klasę Thread, implementujesz interfejs Runnable lub używasz wysokopoziomowych narzędzi takich jak ExecutorService (o nich — w następnej lekcji).
UWAGA! Wielowątkowość nie gwarantuje, że twoje zadania faktycznie wykonują się jednocześnie. Wszystko zależy od tego, ile masz rdzeni w procesorze. Jeśli rdzeń jest tylko jeden, to wątki po prostu szybko „przełączają się” między sobą — tak szybko, że człowiekowi wydaje się, jakby wszystko działo się równocześnie. W rzeczywistości procesor wykonuje tylko jeden wątek w danym momencie, a pozostałe czekają na swoją kolej.
Równoległość: gdy zadania faktycznie wykonują się równocześnie
Równoległość to sytuacja, gdy twój kod naprawdę wykonuje się jednocześnie na kilku rdzeniach procesora. Jeśli masz nowoczesny komputer z 4, 8, 16 rdzeniami — możesz realnie przyspieszyć przetwarzanie dużych zadań, dzieląc je na niezależne części i rozdzielając po rdzeniach.
Jeśli posłużyć się analogią, to wielowątkowość jest wtedy, gdy masz jednego kucharza, który szybko przełącza się między gotowaniem barszczu, smażeniem kotletów i krojeniem sałatki. Równoległość — to gdy masz od razu kilku kucharzy i każdy odpowiada za swoje danie.
Jaka jest różnica w praktyce?
Wielowątkowość dotyczy wygody i responsywności. Używasz wielu wątków, aby program się nie „zawieszał”: jeden wątek czeka na sieć, drugi rysuje interfejs, trzeci coś oblicza. Wszystko działa równolegle w odczuciu użytkownika, ale niekoniecznie jednocześnie.
Równoległość dotyczy szybkości. Tutaj rzeczywiście kilka rdzeni procesora wykonuje różne części zadania jednocześnie, aby szybciej uzyskać wynik.
Innymi słowy: wielowątkowość pomaga zorganizować pracę, a równoległość — ją przyspieszyć.
Ważne:
Wielowątkowość jest potrzebna zawsze, gdy są zadania, które można wykonywać niezależnie.
Równoległość jest potrzebna, gdy chcesz przyspieszyć obliczenia dzięki realnemu rozdzieleniu pracy między rdzenie.
Przykład: przetwarzanie dużej tablicy
Wyobraźmy sobie, że mamy tablicę z 10 milionami liczb i chcemy policzyć sumę wszystkich elementów.
Sekwencyjnie:
Jeden wątek przechodzi po całej tablicy i sumuje elementy. Prosto i niezawodnie, ale wolno.
Wielowątkowo (ale na jednym rdzeniu):
Dzielisz tablicę na 4 części, tworzysz 4 wątki, każdy liczy swoją część. Ale jeśli masz tylko jeden rdzeń, wątki będą działać po kolei — przyspieszenia nie będzie, a narzuty związane z przełączaniem wątków mogą nawet spowolnić program.
Równolegle (na wielu rdzeniach):
Dzielisz tablicę na 4 części, uruchamiasz 4 wątki i każdy wątek naprawdę pracuje na swoim rdzeniu. Wynikowa suma powstaje z 4 części. To naprawdę szybsze — zwłaszcza przy dużych wolumenach danych.
Za to implementacja sekwencyjnego przetwarzania tablicy jest bardzo prosta, już wielokrotnie pisaliście tego typu programy:
// Przykład: sekwencyjne przetwarzanie tablicy
int[] arr = new int[10_000_000];
// ... wypełnianie tablicy ...
long sum = 0;
for (int x : arr) {
sum += x;
}
System.out.println(sum);
Wersje wielowątkowe i równoległe są nieco trudniejsze; omówimy je na kolejnych wykładach z użyciem nowoczesnych narzędzi.
2. Po co nam równoległość
Współczesne procesory już dawno wyszły poza ramy jednego rdzenia. Nawet twój smartfon najpewniej ma co najmniej cztery, a komputery stacjonarne i serwery — osiem, szesnaście, trzydzieści dwa i więcej. Jeśli aplikacja potrafi wykorzystać wszystkie te rdzenie, może działać wielokrotnie szybciej.
Dawniej wydajność procesorów rosła dzięki zwiększaniu częstotliwości taktowania — mniej więcej do połowy lat 2000 faktycznie to działało. Jednak wzrost częstotliwości napotkał ograniczenia fizyczne i wtedy zaczęła się nowa era — systemy wieloprocesorowe i wielordzeniowe. Teraz wygrywają programy, które potrafią efektywnie rozdzielać pracę między rdzenie.
Gdzie równoległość naprawdę przyspiesza?
- Przetwarzanie dużych danych: analiza logów, statystyka, agregacje — wszystko, co można podzielić na niezależne części.
- Renderowanie, przetwarzanie obrazów i wideo: każdy piksel lub fragment można przetwarzać osobno.
- Obliczenia naukowe, modelowanie: zadania matematyczne, symulacje, trenowanie modeli.
- Aplikacje serwerowe: równoczesna obsługa wielu klientów.
- Aplikacje reaktywne: gdy trzeba szybko reagować na wiele zdarzeń, nie blokując głównego wątku.
Kiedy równoległość nie pomaga?
- Gdy zadanie jest małe, narzuty na uruchomienie równoległości mogą być większe niż zysk.
- Gdy zadania nie da się podzielić na niezależne części (np. gdy każdy krok zależy od poprzedniego).
- Gdy jest wiele wspólnych zasobów (np. ten sam plik) i wątki zaczynają sobie przeszkadzać.
3. Typowe zadania dla równoległości
Przyjrzyjmy się, jakie zadania najczęściej „rozdaje się” na rdzenie.
Obliczenia masowe
- Sumowanie, szukanie maksimum/minimum, liczenie statystyk dla dużej tablicy.
- Przykład: policzyć średnią temperaturę z miliona czujników.
Przetwarzanie kolekcji
- Filtrowanie, sortowanie, przekształcanie dużych list (np. przetwarzanie zamówień sklepu internetowego).
- Przykład: wybrać wszystkie zamówienia droższe niż 10 000 rubli i posortować je według daty.
Renderowanie i przetwarzanie grafiki
- Zastosować filtr do wszystkich pikseli obrazu (np. zrobić czarno-białym).
- Każdy piksel można przetwarzać niezależnie — idealny przypadek dla równoległości.
Analiza danych, big data
- MapReduce, agregacje, liczenie statystyk na ogromnych wolumenach danych.
- Przykład: przetwarzanie logów z całego roku w celu wykrycia anomalii.
Przykład: równoległe obliczanie sumy
Załóżmy, że mamy tablicę z 1 milionem liczb. Można podzielić ją na 4 części i policzyć sumę każdej części w osobnym wątku, a potem zsumować wyniki.
4. Problemy i wyzwania równoległości
Trudność debugowania
Gdy kod działa w wielu wątkach, błędy mogą ujawniać się tylko w rzadkich przypadkach, gdy wątki „przecinają się” w szczególny sposób. Czasem błąd pojawia się raz na 1000 uruchomień — i bardzo trudno go uchwycić.
Wyścigi danych (race condition)
Jeśli kilka wątków jednocześnie zmienia tę samą zmienną lub obiekt — możliwe są niepoprawne wyniki. Na przykład dwa wątki równocześnie zwiększają licznik, a wartość końcowa okazuje się mniejsza od oczekiwanej.
Synchronizacja
Aby uniknąć wyścigów, należy synchronizować dostęp do wspólnych danych — przez słowo kluczowe synchronized, blokady, zmienne atomowe i inne narzędzia. To komplikuje kod i może prowadzić do innych problemów (np. deadlock — wzajemna blokada wątków).
Równoważenie obciążenia
Jeśli podzieliłeś zadanie na 4 części, a jedna z nich okazała się znacznie cięższa od pozostałych — trzy wątki już ukończyły pracę i się nudzą, a czwarty wciąż pracuje. W efekcie przyspieszenia nie ma.
Narzuty
Uruchamianie wątków, przełączanie między nimi, synchronizacja — to wszystko wymaga czasu. Jeśli zadanie jest małe, równoległość tylko spowolni wykonanie.
Tabela: porównanie podejść
| Podejście | Kiedy działa szybko | Kiedy spowalnia | Przykład zastosowania |
|---|---|---|---|
| Sekwencyjny (1 wątek) | Małe zadania, prosta logika | Duże zbiory danych | Przetwarzanie 10 linii |
| Wielowątkowość (na 1 rdzeniu) | Zadania asynchroniczne (oczekiwanie IO) | Zadania CPU-bound (ograniczone przez obliczenia procesora) na 1 rdzeniu | Równoczesne pobieranie plików |
| Równoległość (wiele rdzeni) | Duże, niezależne zadania | Małe zadania, silne zależności | Przetwarzanie dużej tablicy |
Wizualizacja: jak to wygląda
// Przetwarzanie sekwencyjne (1 wątek)
[Zadanie 1][Zadanie 2][Zadanie 3][Zadanie 4]
// Wielowątkowość na jednym rdzeniu (logika przełączania)
[Zadanie 1] [Zadanie 2] [Zadanie 3] [Zadanie 4]
(ale realnie działa tylko jedno naraz, pozostałe czekają)
// Równoległość na czterech rdzeniach
[Zadanie 1] [Zadanie 2] [Zadanie 3] [Zadanie 4]
(wszystko wykonuje się jednocześnie)
5. Typowe błędy przy próbach równoległego przetwarzania
Błąd nr 1: Równoleglić wszystko jak leci. Wielu początkujących myśli: „Im więcej wątków — tym szybciej!”. W rzeczywistości tak nie jest. Jeśli zadań jest mało albo są zbyt proste — zysku brak, a czasem program bywa nawet wolniejszy.
Błąd nr 2: Ignorowanie synchronizacji. Jeśli kilka wątków pracuje na tych samych danych bez synchronizacji — pojawią się wyścigi danych, popsuć się może logika i wystąpią błędy, które trudno uchwycić.
Błąd nr 3: Równoległość dla samej równoległości. Równoległość nie jest celem samym w sobie. Jest potrzebna, gdy istnieją realne zadania, które da się efektywnie podzielić na niezależne części.
Błąd nr 4: Nieuwzględnianie specyfiki zadania. Niektórych zadań w ogóle nie da się zrównoleglić (np. gdy krok N+1 zależy od wyniku kroku N). W takich przypadkach równoległość nie da przewagi.
Błąd nr 5: Nieuwzględnianie narzutów. Uruchamianie wątków, przełączanie między nimi, zbieranie wyników — to wszystko wymaga czasu. Dla małych zadań ten czas może być większy niż czas samej pracy.
GO TO FULL VERSION