Paralelné programovanie v Pythone

Funkcie a dátové štruktúry pre paralelné programovanie v Pythone

  • vlákna
  • procesy
  • queue, zásobník
  • synchronizácia - zámky

Multithreading

Thread = vlákno.

Jedna aplikácia môže mať viac vlákien.

Program
 ├── Thread 1
 ├── Thread 2
 └── Thread 3

Vytvorenie vlákna

import threading

def worker():
    print("Pracujem")

t = threading.Thread(target=worker)

t.start()
t.join()

Výhody

  • jednoduchšie než procesy
  • zdieľaná pamäť
  • vhodné pre I/O operácie

Nevýhody

  • problémy so synchronizáciou
  • race conditions
  • GIL

Multiprocessing

Namiesto viacerých vlákien vytvoríme viac procesov.

Každý proces má:

  • vlastnú pamäť
  • vlastný Python interpreter

Príklad

from multiprocessing import Process

def worker():
    print("Hello")

p = Process(target=worker)

p.start()
p.join()

Výhody

✔ využije viac CPU jadier

✔ vhodné pre výpočty

Nevýhody

❌ väčšia spotreba pamäte

❌ drahšia komunikácia medzi procesmi

Funkcionálne programovanie a paralelizmus

Jednou z výhod funkcionálneho programovania je, že čisté funkcie sa dajú jednoducho spúšťať paralelne.

Prečo?

  • nepracujú so zdieľaným stavom
  • nemenia globálne premenné
  • nemajú vedľajšie efekty

To znamená, že viac procesov môže vykonávať tú istú funkciu nezávisle od seba.

Prečo sa FP hodí na multiprocessing?

Čistá funkcia:

def square(x):
    return x * x
  • dostane vstup
  • vráti výstup
  • nič nemení

Takéto funkcie sa veľmi dobre paralelizujú.

Problematický príklad:

counter = 0

def increment(x):
    global counter
    counter += 1
    return x

Ak by túto funkciu spúšťalo viac procesov súčasne:

  • vznikajú problémy so synchronizáciou
  • výsledok môže byť nepredvídateľný

Multiprocessing v Pythone

Modul multiprocessing umožňuje využívať viac CPU jadier.

from multiprocessing import Pool

Vytvoríme funkciu:

def square(x):
    return x * x

Pool.map()

Podobne ako klasické map(), ale úloha sa rozdelí medzi viac procesov.

from multiprocessing import Pool

def square(x):
    return x * x

numbers = [1, 2, 3, 4, 5]

with Pool() as pool:
    result = pool.map(square, numbers)

print(result)

Výstup:

[1, 4, 9, 16, 25]

map() vs Pool.map()

Bežné map():

result = list(map(square, numbers))

Spracovanie:

1 → square
2 → square
3 → square
4 → square

v jednom procese.

Pool.map():

with Pool() as pool:
    result = pool.map(square, numbers)

Spracovanie:

CPU1 → square(1)
CPU2 → square(2)
CPU3 → square(3)
CPU4 → square(4)

súčasne na viacerých jadrách.

Praktický príklad

Simulácia náročného výpočtu:

import time

def heavy_task(x):
    time.sleep(1)
    return x * x

Bez multiprocessing:

result = list(map(heavy_task, range(8)))

Čas:

≈ 8 sekúnd

S multiprocessing:

from multiprocessing import Pool

with Pool() as pool:
    result = pool.map(heavy_task, range(8))

Na 4-jadrovom CPU:

≈ 2 sekundy

GIL a multiprocessing

Python má Global Interpreter Lock (GIL).

Pre CPU náročné úlohy:

threading

from threading import Thread

často neprinesie zrýchlenie.

multiprocessing

from multiprocessing import Pool

spúšťa viac samostatných procesov a dokáže využiť viac jadier procesora.

Kedy používať multiprocessing?

✔ vhodné:

  • spracovanie veľkých dát
  • matematické výpočty
  • analýza obrázkov
  • simulácie
  • machine learning preprocessing

❌ nevhodné:

  • jednoduché operácie
  • krátke výpočty
  • úlohy s veľkým množstvom komunikácie medzi procesmi

Zhrnutie

  • Funkcionálne programovanie podporuje paralelizmus.
  • Čisté funkcie sa jednoducho spúšťajú v samostatných procesoch.
  • Python poskytuje modul multiprocessing.
  • Pool.map() je paralelná verzia map().
  • Pri CPU náročných úlohách je multiprocessing často lepšia voľba než threading.

GIL – Global Interpreter Lock

Jedna z najdôležitejších vlastností CPythonu.

Čo robí?

V jednom procese môže Python bytecode vykonávať iba jedno vlákno naraz.

Thread 1
Thread 2
Thread 3

      ↓

    GIL

      ↓

CPU

Dôsledok

CPU náročný kód:

for i in range(100_000_000):
    pass

nebude výrazne rýchlejší pri použití viacerých threadov.

Preto:

Typ úlohy Odporúčanie
I/O-bound Async alebo Threads
CPU-bound Multiprocessing

Race Condition

Vzniká, keď viac vlákien mení rovnaké dáta.

Príklad:

counter += 1

Ak to vykonajú dve vlákna naraz, výsledok nemusí byť správny.

Ukážka problému

import threading

counter = 0

def increment():
    global counter

    for _ in range(100000):
        counter += 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print(counter)

Očakávame:

200000

Ale nemusíme ho dostať.

Mutex (Lock)

Mutex zabezpečuje, že kritickú sekciu vykonáva iba jedno vlákno.

Príklad

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter

    for _ in range(100000):
        with lock:
            counter += 1

Kritická sekcia

Časť programu, kde sa pracuje so zdieľanými dátami.

with lock:
    counter += 1

Počas vykonávania:

  • ostatné vlákna čakajú
  • nedochádza k poškodeniu dát

Async vs Threading vs Multiprocessing

Vlastnosť Async Threading Multiprocessing
I/O operácie ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
CPU operácie ⭐⭐ ⭐⭐⭐⭐⭐
Spotreba pamäte nízka stredná vysoká
Zdieľaná pamäť áno áno nie
GIL problém nie áno nie
Typické použitie API, DB, siete súbory, siete AI, výpočty

Kompletné zhrnutie

Moderný Python ponúka tri hlavné prístupy ku konkurentnému programovaniu:

Asyncio

async def task():
    await asyncio.sleep(1)
  • jeden thread
  • event loop
  • ideálne pre I/O

Threading

threading.Thread(...)
  • viac vlákien
  • zdieľaná pamäť
  • vhodné pre I/O

Multiprocessing

Process(...)
  • viac procesov
  • využitie viacerých CPU jadier
  • vhodné pre náročné výpočty

Dôležité pravidlo

Ak program väčšinu času čaká → použij async alebo threading.

Ak program väčšinu času počíta → použij multiprocessing.

Reload?