Funkcie a dátové štruktúry pre paralelné programovanie v Pythone
Thread = vlákno.
Jedna aplikácia môže mať viac vlákien.
Program
├── Thread 1
├── Thread 2
└── Thread 3
import threading
def worker():
print("Pracujem")
t = threading.Thread(target=worker)
t.start()
t.join()
Namiesto viacerých vlákien vytvoríme viac procesov.
Každý proces má:
from multiprocessing import Process
def worker():
print("Hello")
p = Process(target=worker)
p.start()
p.join()
✔ využije viac CPU jadier
✔ vhodné pre výpočty
❌ väčšia spotreba pamäte
❌ drahšia komunikácia medzi procesmi
Jednou z výhod funkcionálneho programovania je, že čisté funkcie sa dajú jednoducho spúšťať paralelne.
Prečo?
To znamená, že viac procesov môže vykonávať tú istú funkciu nezávisle od seba.
Čistá funkcia:
def square(x):
return x * x
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:
Modul multiprocessing umožňuje využívať viac CPU jadier.
from multiprocessing import Pool
Vytvoríme funkciu:
def square(x):
return x * x
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]
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.
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
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.
✔ vhodné:
❌ nevhodné:
multiprocessing.Pool.map() je paralelná verzia map().multiprocessing často lepšia voľba než threading.Jedna z najdôležitejších vlastností CPythonu.
V jednom procese môže Python bytecode vykonávať iba jedno vlákno naraz.
Thread 1
Thread 2
Thread 3
↓
GIL
↓
CPU
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.
| Typ úlohy | Odporúčanie |
|---|---|
| I/O-bound | Async alebo Threads |
| CPU-bound | Multiprocessing |
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.
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 zabezpečuje, že kritickú sekciu vykonáva iba jedno vlákno.
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1
Časť programu, kde sa pracuje so zdieľanými dátami.
with lock:
counter += 1
Počas vykonávania:
| 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 |
Moderný Python ponúka tri hlavné prístupy ku konkurentnému programovaniu:
async def task():
await asyncio.sleep(1)
threading.Thread(...)
Process(...)
Ak program väčšinu času čaká → použij async alebo threading.
Ak program väčšinu času počíta → použij multiprocessing.