CodeGym /Kursy /JAVA 25 SELF /Wprowadzenie do równoległości

Wprowadzenie do równoległości

JAVA 25 SELF
Poziom 54, Lekcja 0
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 54, lekcja 0
Niedostępne
Nadanie nazwy cyfrowemu pracownikowi 🤖
Nadanie nazwy cyfrowemu pracownikowi 🤖
1
Zadanie
JAVA 25 SELF, poziom 54, lekcja 0
Niedostępne
Audyt pracy ramion robotycznych 📊
Audyt pracy ramion robotycznych 📊
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION