Obsah
3. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více vláknech
4. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více procesech
5. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více interpretrech
6. Benchmark zjišťující vlastnosti výpočtů realizovaných asynchronními úlohami
8. Doba vykonání 100 krátkodobých úloh při nastavení úrovně souběžnosti na 100
9. Doba vykonání 100 déletrvajících úloh při nastavení úrovně souběžnosti na 10
10. Doba vykonání 1000 krátkodobých úloh při nastavení úrovně souběžnosti na 100
11. Doba vykonání 1000 déletrvajících úloh při nastavení úrovně souběžnosti na 100
12. Paměťové nároky všech čtyř variant benchmarků
13. Změřené výsledky: paměťové nároky benchmarků
14. Benchmarky provádějící náročnější výpočty bez I/O operací
15. Zdrojové kódy všech čtyř upravených benchmarků
18. Odkazy na články s problematikou souběžnosti a paralelnosti v Pythonu
19. Repositář s demonstračními příklady
1. Nové vlastnosti Pythonu 3.14 v praxi: rychlost a paměťové nároky aplikací využívajících více více souběžných úloh
V předchozím článku o Pythonu 3.14 (tedy o nejnovější v současnosti oficiálně dostupné verzi CPythonu) jsme se zaměřili na popis realizace souběžných a v některých případech i paralelních úloh. Připomeňme si ve stručnosti, že pro realizaci takových úloh je možné využít čtyři rozdílné technologie:
- Spuštění úloh asynchronně, což například umožní, aby se (z pohledu času) překrývaly například vstupně-výstupní operace atd. Pro tento účel slouží v Pythonu klíčová slova async a await, které byly přidány již do Pythonu 3.5 (a asyncio.run v Pythonu 3.7).
- Spuštění úloh v samostatných vláknech. Konkrétní chování a způsob provedení souběžných výpočtů závisí do značné míry na tom, zda je Python 3.14 přeložen s vestavěným GILem či nikoli. Pokud je GIL použit (standardní interpret), budou úlohy GILem synchronizovány při provádění mnoha operací.
- Spuštění úloh v samostatných procesech. Zde se již jedná o řešení, které není omezeno GILem, protože procesy řídí samotné jádro operačního systému, ovšem pravděpodobně za možnost paralelizace zaplatíme větší spotřebou paměti a pomalejším spouštěním úloh.
- A konečně spuštění úloh v samostatných interpretrech, přičemž každý interpret poběží v samostatném vláknu. Teoreticky by se mělo toto řešení nacházet na pomezí mezi výše zmíněným multithreadingem a multiprocesingem. Toto řešení je dostupné až od Pythonu 3.14.
V dnešním článku se tato řešení pokusíme porovnat z pohledu celkové rychlosti, rychlosti inicializace a spuštění úloh i z hlediska celkové spotřeby operační paměti.
2. Základní podoba benchmarků
Pro porovnání všech čtyř výše zmíněných technologií určených pro spouštění většího množství souběžných či paralelně běžících úloh nejprve použijeme čtyři základní benchmarky, které budou simulovat skutečné výpočty zavoláním funkce time.sleep popř. její asynchronní varianty. Ve všech benchmarcích budou parametry spouštěných úloh předávány s využitím sdílené fronty (queue) a fronta bude použita i pro ukončení činnosti jednotlivých workerů. Pokaždé je sice fronta realizována jinou technologií, ovšem z pohledu programátora je její chování podobné.
Všechny čtyři benchmarky jsem se snažil vytvořit takovým způsobem, aby se i přes odlišnou technologii spouštění úloh jejich zápis do značné míry podobal. Řízení benchmarků se provádí změnou čtyř globálních proměnných:
# počet souběžně spuštěných vláken, procesů, asychronních úloh nebo interpretrů CONCURRENCY_LEVEL = 100 # celkový počet úloh, které je nutné vypočítat TASKS = 1000 # má se na konci benchmarku čekat na stisk klávesy? (například pro zjištění obsazené paměti) WAIT_FOR_KEY = False # simulace reálné práce – na tuto dobu bude pozastaveno vlákno, # proces, asynchronní úloha nebo interpret SLEEP_AMOUNT = 1
3. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více vláknech
První implementace benchmarku je založena na vykonání úloh ve větším množství vláken – jedná se tedy o klasický multithreading. Vlákna jsou nejdříve vytvořena a poté je v rámci každého z nich spuštěna funkce worker. Parametry úloh jsou předávány přes frontu a předáním příkazu/parametru „quit“ je worker ukončen:
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových vláknech
# - komunikace mezi vlákny s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
from queue import Queue
from threading import Thread
import time
def worker(name, q):
"""Worker spuštěný několikrát v samostatných vláknech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Thread '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Thread '{name}' is about to quit")
return
if SLEEP_AMOUNT > 0:
time.sleep(SLEEP_AMOUNT)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi vlákny
q = Queue()
ts = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Thread #{i}"
ts.append(Thread(target=worker, daemon=True, name=name, args=[name, q]))
# spuštění vláken
for t in ts:
t.start()
print("Sending data to other threads")
# komunikace s vlákny přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
input()
print("Asking other threads to finish")
# příkaz pro ukončení vláken
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other threads")
# čekání na zpracování všech zpráv ve frontě
for t in ts:
t.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
4. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více procesech
Druhá varianta benchmarku používá namísto většího množství vláken pro spuštění workerů samostatné procesy. V takovém případě je přepínání mezi procesy prováděno operačním systémem a procesy by měly běžet do značné míry nezávisle na sobě (ovšem pochopitelně se provádí synchronizace při komunikaci pomocí fronty). Takto upravený benchmark vypadá následovně:
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových procesech
# - komunikace mezi procesy s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
import time
from multiprocessing import Process, Queue, freeze_support
def worker(name, q):
"""Worker spuštěný několikrát v samostatných procesech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Process '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Process '{name}' is about to quit")
return
if SLEEP_AMOUNT > 0:
time.sleep(SLEEP_AMOUNT)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
freeze_support()
# vytvoření fronty pro komunikaci mezi procesy
q = Queue()
ps = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Process #{i}"
ps.append(Process(target=worker, args=(name, q)))
# spuštění procesů
for p in ps:
p.start()
print("Sending data to other processes")
# komunikace s procesy přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
input()
print("Asking other processes to finish")
# příkaz pro ukončení procesů
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other processes")
# čekání na ukončení procesů
for p in ps:
p.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
5. Benchmark zjišťující vlastnosti výpočtů realizovaných ve více interpretrech
Třetí varianta benchmarku bude funkční pouze v Pythonu 3.14. Úlohy jsou spouštěny v rámci samostatných interpretů, přičemž každý interpret běží v samostatném vlákně. A pro komunikaci mezi workery se pochopitelně opět používá fronta (která ovšem interně vypadá odlišně, než fronty z předchozích dvou benchmarků):
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových interpretrech
# - komunikace mezi interpretry s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
import time
from concurrent import interpreters
def worker(name, q):
"""Worker spuštěný několikrát v samostatných interpretrech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Interpreter '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Interpreter '{name}' is about to quit")
return
if SLEEP_AMOUNT > 0:
time.sleep(SLEEP_AMOUNT)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi interpretry
q = interpreters.create_queue()
ins = []
# vytvoření interpretrů
for i in range(CONCURRENCY_LEVEL):
name = f"Interpreter #{i}"
ins.append(interpreters.create().call_in_thread(worker, name, q))
print("Sending data to other interpreters")
# komunikace s interpretry přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
print("Asking other interpreters to finish")
# příkaz pro ukončení procesů
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
if WAIT_FOR_KEY:
input()
print("Waiting for other interpreters")
# čekání na ukončení interpretrů
for i in ins:
i.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
6. Benchmark zjišťující vlastnosti výpočtů realizovaných asynchronními úlohami
Poslední benchmark je naprogramován odlišným způsobem, protože v něm využíváme workery spouštěné v asynchronním kódu. Je zde tedy nutné používat klíčová slova async a await a i hlavní program je realizován asynchronní funkcí. I „čekání“ na dokončení úlohy je realizováno asynchronní operací sleep (to ovšem není férové, proto si ještě ukážeme nepatrně komplikovanější benchmarky):
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh asynchronně
# - komunikace mezi procesy s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
from asyncio import Queue, sleep, run, gather, create_task
import time
async def worker(name, q):
"""Worker spuštěný několikrát asynchronně."""
while not q.empty():
# čtení příkazů z fronty
cmd = await q.get()
print(f"Task '{name}' received command '{cmd}'")
await sleep(SLEEP_AMOUNT)
async def main():
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi úlohami
queue = Queue()
print("Sending data to async tasks")
# komunikace s úlohami přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
await queue.put("command {}".format(i))
print("Waiting for all tasks")
aws = [create_task(worker(f"Task #{i}", queue)) for i in range(CONCURRENCY_LEVEL)]
await gather(*aws)
if WAIT_FOR_KEY:
input()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
run(main())
7. Výsledky benchmarků
Podívejme se nyní na výsledky benchmarků pro různé nastavení celkového počtu úloh, které se mají vykonat, počtu souběžně pracujících workerů a navíc i minimální doby trvání úloh. Tím si ověříme způsob chování Pythonu v případě, že workery běží v samostatných procesech, ve vláknech, v samostatných interpretrech či jako asynchronní úlohy. Posléze benchmarky upravíme do takové podoby, aby se namísto pouhého čekání (operace sleep) vykonával skutečný výpočet.
8. Doba vykonání 100 krátkodobých úloh při nastavení úrovně souběžnosti na 100
Nejprve všechny benchmarky nastavíme tak, aby se vykonalo pouze 100 úloh, které navíc trvají co nejkratší dobu. Úroveň souběžnosti/paralelnosti je nastavena taktéž na hodnotu 100, což znamená, že podle benchmarku je:
- každá úloha vykonána ve svém vlastním vláknu
- každá úloha vykonána ve svém vlastním procesu
- každá úloha vykonána ve své vlastní asynchronní funkci
- každá úloha vykonána ve svém vlastním interpretu
Tyto benchmarky tedy měří primárně rychlost vytvoření workerů.
Nastavení všech benchmarků:
CONCURRENCY_LEVEL = 100 TASKS = 100 WAIT_FOR_KEY = False SLEEP_AMOUNT = 0
Výsledky:
| Typ souběžnosti | Celkový čas vykonání |
|---|---|
| Async | 0,0115 |
| Interpreters | 3,9117 |
| Processes | 0,1410 |
| Threads | 0,0620 |
9. Doba vykonání 100 déletrvajících úloh při nastavení úrovně souběžnosti na 10
V dalším kroku konfiguraci benchmarků změníme. Bude se sice opět provádět celkem sto úloh, ovšem úroveň souběžnosti se zmenší na 10 vláken/procesů/interpetů/asychronních funkcí a každá úloha bude trvat přibližně jednu sekundu. Takto nastavené benchmarky tedy měří spíše to, do jaké míry běží úlohy paralelně. Teoretická ideální doba běhu bude rovna 100/10=10 sekund.
Nastavení všech benchmarků:
CONCURRENCY_LEVEL = 10 TASKS = 100 WAIT_FOR_KEY = False SLEEP_AMOUNT = 1
Výsledky:
| Typ souběžnosti | Celkový čas vykonání |
|---|---|
| Async | 10,0192 |
| Interpreters | 10,3868 |
| Processes | 10,1612 |
| Threads | 10,01469 |
10. Doba vykonání 1000 krátkodobých úloh při nastavení úrovně souběžnosti na 100
Nyní se pokusíme počet úloh zvýšit na jeden tisíc, ovšem úroveň souběžnosti (počet vláken/procesů/interpretrů/asynchronních úloh) bude omezena na 100. Jednotlivé úlohy budou prováděny velmi rychle – prakticky bez čekání. Pouze u benchmarku s asynchronními úlohami se čeká na dokončení operace sleep(0), což umožní přepnutí úloh:
| Typ souběžnosti | Celkový čas vykonání |
|---|---|
| Async | 0,0122 |
| Interpreters | 3,8573 |
| Processes | 0,5456 |
| Threads | 0,1016 |
11. Doba vykonání 1000 déletrvajících úloh při nastavení úrovně souběžnosti na 100
Poslední variantou konfigurace benchmarků bude jejich nastavení takovým způsobem, aby se provedlo tisíc úloh s nastavením úrovně souběžnosti na 100. To znamená, že by každý worker (bude jich připraveno celkem 100) měl provést deset úloh. Každá úloha poběží minimálně jednu sekundu, čímž se simuluje reálná práce (tedy s výjimkou asynchronních úloh, kde čekání vede k přepnutí úlohy):
Výsledky benchmarků:
| Typ souběžnosti | Celkový čas vykonání |
|---|---|
| Async | 10,03398 |
| Interpreters | 13,7186 |
| Processes | 10,5159 |
| Threads | 10,0677 |
12. Paměťové nároky všech čtyř variant benchmarků
Prozatím jsme změřili, jak dlouho trvá vykonání určitého počtu úloh při použití různých technologií. Zajímavé (a popravdě řečeno i dost neočekávané) je, že spouštění úloh v samostatných interpretrech, což je nová technologie Pythonu 3.14, prozatím vychází nejhůře. Je to způsobeno tím, že nejvíce času se stráví inicializací interpretrů – je zde tedy prostor (a to dosti velký) pro další vylepšování v dalších verzích Pythonu.
Ovšem ještě budeme muset změřit paměťové nároky jednotlivých variant benchmarků, protože v praxi se může jednat taktéž o limitující faktor. Dá se předpokládat, že největší paměťové nároky bude mít řešení s větším množstvím procesů, ovšem zajímavé bude zjištění, jaké paměťové nároky má technologie více interpretrů.
13. Změřené výsledky: paměťové nároky benchmarků
Otázkou je, jakým způsobem se mají paměťové nároky měřit. Můžeme například zjistit alokovanou oblast virtuální paměti nebo RSS (což je hodnota, která je asi nejbližší skutečně využívané paměti). Spotřeba paměti bude vyšší u benchmarků s multiprocesingem, protože každý proces znamená nastartování nového interpretru Pythonu. Úlohy spouštěné ve vláknech běží v rámci jednoho procesu a totéž platí i pro úlohy spouštěné ve vlastních interpretrech – každý interpret běží ve vláknu v rámci jediného procesu.
Výsledky pro benchmark s asynchronními úlohami. Bylo naalokováno 1259MB virtuální paměti, ovšem RSS je jen 23,6 MB. Běží, podle očekávání, jen jediný proces Pythonu:
Výsledky pro benchmark s úlohami běžícími v samostatných vláknech. RSS (jediného procesu) je kupodivu pouze 14,2 MB. Na screenshotu jsou zeleně zobrazena vlákna, nikoli procesy:
Výsledky pro benchmark s úlohami běžícími v samostatných procesech. Nyní již vidíme mnoho nových procesů s vlastní alokovanou pamětí (17MB pro každý takový proces) a navíc ještě dva další procesy, které vše koordinují. Spotřeba paměti je v tomto případě mnohem vyšší, než u dalších benchmarků:
A konečně výsledky benchmarku pro deset interpretrů běžících v samostatných vláknech. Nyní sice běží jen jeden proces, ovšem jeho paměťové nároky jsou 79MB, což je více, než u běžného multithreadingu (zeleně jsou opět zobrazena jednotlivá vlákna):
| Typ souběžnosti | Celková RSS |
|---|---|
| Async | 23,6 MB |
| Interpreters | 79,0 MB |
| Processes | 204 MB (přibližný výpočet) |
| Threads | 14,2 MB |
V tomto ohledu tedy může být použití více interpretrů výhodnější, než klasický multiprocesing.
14. Benchmarky provádějící náročnější výpočty bez I/O operací
Nyní naše čtyři benchmarky nepatrně upravíme, a to takovým způsobem, aby se v nich prováděly nějaké intenzivnější výpočty. Přitom zajistíme, aby se NEpoužívaly vstupně-výstupní operace ani operace sleep, která sice z pohledu jediné úlohy skutečně simuluje práci, ovšem například v případě asynchronních úloh umožní přepínání bez vykonání skutečné činnosti. Výpočtem, které budou benchmarky provádět, je realizace algoritmu pro bublinkové řazení. Tento algoritmus je implementačně jednoduchý, s velkou pravděpodobností nevyužívá SIMD operace (takže procesor nebude zpomalován – viz Intel :-) a skutečně vytíží procesorové jádro nebo jádra:
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
for i in range(size - 1, 0, -1):
for j in range(i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
15. Zdrojové kódy všech čtyř upravených benchmarků
Bubble sort: asynchronní verze
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh asynchronně
# - komunikace mezi procesy s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
import random
from asyncio import Queue, sleep, run, gather, create_task
import time
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
for i in range(size - 1, 0, -1):
for j in range(i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
async def worker(name, q):
"""Worker spuštěný několikrát asynchronně."""
while not q.empty():
# čtení příkazů z fronty
cmd = await q.get()
print(f"Task '{name}' received command '{cmd}'")
bubble_sort(1000)
async def main():
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi úlohami
queue = Queue()
print("Sending data to async tasks")
# komunikace s úlohami přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
await queue.put("command {}".format(i))
print("Waiting for all tasks")
aws = [create_task(worker(f"Task #{i}", queue)) for i in range(CONCURRENCY_LEVEL)]
await gather(*aws)
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
run(main())
Bubble sort: verze s více interpretry běžícími v samostatných vláknech
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových interpretrech
# - komunikace mezi interpretry s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
import random
import time
from concurrent import interpreters
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
for i in range(size - 1, 0, -1):
for j in range(i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
def worker(name, q):
"""Worker spuštěný několikrát v samostatných interpretrech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Interpreter '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Interpreter '{name}' is about to quit")
return
bubble_sort(1000)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi interpretry
q = interpreters.create_queue()
ins = []
# vytvoření interpretrů
for i in range(CONCURRENCY_LEVEL):
name = f"Interpreter #{i}"
ins.append(interpreters.create().call_in_thread(worker, name, q))
print("Sending data to other interpreters")
# komunikace s interpretry přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
print("Asking other interpreters to finish")
# příkaz pro ukončení procesů
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other interpreters")
# čekání na ukončení interpretrů
for i in ins:
i.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
Bubble sort: verze běžící ve více procesech
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových procesech
# - komunikace mezi procesy s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
import random
import time
from multiprocessing import Process, Queue, freeze_support
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
for i in range(size - 1, 0, -1):
for j in range(i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
def worker(name, q):
"""Worker spuštěný několikrát v samostatných procesech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Process '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Process '{name}' is about to quit")
return
bubble_sort(1000)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
freeze_support()
# vytvoření fronty pro komunikaci mezi procesy
q = Queue()
ps = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Process #{i}"
ps.append(Process(target=worker, args=(name, q)))
# spuštění procesů
for p in ps:
p.start()
print("Sending data to other processes")
# komunikace s procesy přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
input()
print("Asking other processes to finish")
# příkaz pro ukončení procesů
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other processes")
# čekání na ukončení procesů
for p in ps:
p.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
Bubble sort: verze běžící ve více vláknech
# Multiprocesing a multithreading v Pythonu:
# - spuštění více úloh v nových vláknech
# - komunikace mezi vlákny s využitím fronty
CONCURRENCY_LEVEL = 100
TASKS = 1000
WAIT_FOR_KEY = False
SLEEP_AMOUNT = 1
import random
from queue import Queue
from threading import Thread
import time
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
for i in range(size - 1, 0, -1):
for j in range(i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
def worker(name, q):
"""Worker spuštěný několikrát v samostatných vláknech."""
while True:
# čtení příkazů z fronty
cmd = q.get()
print(f"Thread '{name}' received command '{cmd}'")
if cmd == "quit":
print(f"Thread '{name}' is about to quit")
return
bubble_sort(1000)
if __name__ == "__main__":
t1 = time.time()
print("Starting")
# vytvoření fronty pro komunikaci mezi vlákny
q = Queue()
ts = []
# vytvoření procesů
for i in range(CONCURRENCY_LEVEL):
name = f"Thread #{i}"
ts.append(Thread(target=worker, daemon=True, name=name, args=[name, q]))
# spuštění vláken
for t in ts:
t.start()
print("Sending data to other threads")
# komunikace s vlákny přes frontu
for i in range(TASKS):
print(f"Sending 'command {i}'")
q.put("command {}".format(i))
if WAIT_FOR_KEY:
input()
print("Asking other threads to finish")
# příkaz pro ukončení vláken
for i in range(CONCURRENCY_LEVEL):
q.put("quit")
print("Waiting for other threads")
# čekání na zpracování všech zpráv ve frontě
for t in ts:
t.join()
print("All work done!")
t2 = time.time()
print(f"Elapsed time: {t2-t1}")
16. Změřené výsledky
V tabulce jsou zobrazeny časy běhu benchmarků zaokrouhlených na celé sekundy. První tři benchmarky vykazují při opakovaném spuštění prakticky stejné hodnoty (jsou stabilní), ovšem řešení založené na více vláknech dosti podstatným způsobem kolísá – ovšem jen tehdy, pokud se použije Python 3.14 s vypnutým GILem. Synchronizační mechanismy použité při zakázaném GILu se z tohoto pohledu chovají dosti nedeterministicky. Dále je – což je pochopitelné – čas asynchronní varianty nejpomalejší, neboť se vlastně úlohy provádí sekvenčně. A tento čas je shodný s benchmarkem založeným na použití více vláken, pokud je povolený GIL (který výpočet „serializuje“):
| Typ souběžnosti | Celkový čas vykonání (bez GILu) | Celkový čas vykonání (s GILem) |
|---|---|---|
| Async | 54 | 54 |
| Interpreters | 13 | 40 |
| Processes | 10 | 10 |
| Threads | 21–45 | 54 |
17. Shrnutí
Nová technologie umožňující spouštění výpočtů v izolovaných interpretrech, z nichž každý běží ve vlastním vlákně, má v současné verzi Pythonu tyto vlastnosti, ke kterým je nutné přihlížet:
- Inicializace a spuštění interpretrů je dosti pomalé, a to i v porovnání se spuštěním více procesů. Tento stav by se však měl postupně zlepšovat.
- Výkon při zakázaném GILu je stabilní a kupodivu vyšší, než při použití klasického multithreadingu, což je poměrně překvapující zjištění.
- Paměťové nároky jsou nižší, než při použití více procesů (logicky), ale vyšší, než u klasického multithreadingu.
- (Asynchronní úlohy jsou vhodné jen pro oblasti s velkým množstvím I/O operací, nikoli pro výpočty).
18. Odkazy na články s problematikou souběžnosti a paralelnosti v Pythonu
Na stránkách Roota jsme se již několikrát setkali s problematikou souběžnosti (concurrency), paralelnosti (parallel run) a asynchronního běhu úloh naprogramovaných v Pythonu. Různé varianty spouštění a řízení více vláken, procesů a asynchronních úloh naleznete v následujících článcích (všechny v článcích uvedené demonstrační příklady by měly být spustitelné i v interpretru Pythonu 3.14 bez povoleného GILu):
- Souběžné a paralelně běžící úlohy naprogramované v Pythonu
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu (2)
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-2/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – Curio a Trio
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-curio-a-trio/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-knihovna-trio/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio (2)
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-knihovna-trio-2/ - Souběžné a paralelně běžící úlohy naprogramované v Pythonu – závěrečné zhodnocení
https://www.root.cz/clanky/soubezne-a-paralelne-bezici-ulohy-naprogramovane-v-pythonu-zaverecne-zhodnoceni/ - Interpret Pythonu bez GILu: vyplatí se odstranění velkého zámku?
https://www.root.cz/clanky/interpret-pythonu-bez-gilu-vyplati-se-odstraneni-velkeho-zamku/ - Nové vlastnosti Pythonu 3.14 v praxi: vliv odstranění GIL a využití více interpretrů
https://www.root.cz/clanky/nove-vlastnosti-pythonu-3–14-v-praxi-vliv-odstraneni-gil-a-vyuziti-vice-interpretru/ - Nové vlastnosti Pythonu 3.14 v praxi: komunikace mezi interpretry
https://www.root.cz/clanky/nove-vlastnosti-pythonu-3–14-v-praxi-komunikace-mezi-interpretry/
19. Repositář s demonstračními příklady
Všechny demonstrační příklady, které byly popsány v dnešním článku, naleznete na adresách:
Demonstrační příklady z předchozího článku jsou vypsány v další tabulce:
Všechny předminule popsané demonstrační příklady jsou vypsány v následující tabulce:
Demonstrační příklady vytvořené pro Python verze 3.14 a popsané v prvním článku najdete v repositáři https://github.com/tisnik/most-popular-python-libs/. Následují odkazy na jednotlivé příklady:
| # | Příklad | Stručný popis | Adresa |
|---|---|---|---|
| 1 | argparse_test.py | skript s definicí přepínačů použitelných na příkazovém řádku | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/argparse_test.py |
| 2 | syntax_error1.py | skript obsahující syntaktické chyby: chybějící či naopak přebývající písmeno v klíčovém slovu nebo identifikátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/syntax_error1.py |
| 2 | syntax_error2.py | skript obsahující syntaktické chyby: chybějící či naopak přebývající písmeno v klíčovém slovu nebo identifikátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/syntax_error2.py |
| 3 | syntax_error3.py | skript obsahující syntaktické chyby: chybějící či naopak přebývající písmeno v klíčovém slovu nebo identifikátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/syntax_error3.py |
| 4 | syntax_error4.py | skript obsahující syntaktické chyby: chybějící či naopak přebývající písmeno v klíčovém slovu nebo identifikátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/syntax_error4.py |
| 5 | syntax_error5.py | skript obsahující syntaktické chyby: chybějící či naopak přebývající písmeno v klíčovém slovu nebo identifikátoru | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/syntax_error5.py |
| 6 | primes.py | realizace výpočtu prvočísel | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/primes.py |
| 7 | test_primes.py | jednotkové testy pro modul primes.py | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/test_primes.py |
| 8 | pep-758-motivation-1.py | zachycení většího množství výjimek v bloku except – motivační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-758-motivation-1.py |
| 9 | pep-758-motivation-2.py | zachycení většího množství výjimek v bloku except – motivační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-758-motivation-2.py |
| 10 | pep-758-usage.py | nový způsob zachycení výjimek definovaný v PEP-758 | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-758-usage.py |
| 11 | pep-758-usage-as.py | klauzule as a nový způsob zachycení výjimek definovaný v PEP-758 | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-758-usage-as.py |
| 12 | pep-765-motivation-1.py | detekce opuštění bloku finally, první demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-765-motivation-1.py |
| 13 | pep-765-motivation-2.py | detekce opuštění bloku finally, druhý demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-765-motivation-2.py |
| 14 | pep-765-motivation-3.py | detekce opuštění bloku finally, třetí demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-765-motivation-3.py |
| 15 | pep-765-motivation-4.py | detekce opuštění bloku finally, čtvrtý demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/pep-765-motivation-4.py |
| 16 | f-string-1.py | rozdíl mezi f-řetězci a t-řetězci, první demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/f-string-1.py |
| 17 | t-string-1.py | rozdíl mezi f-řetězci a t-řetězci, první demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/t-string-1.py |
| 18 | f-string-2.py | rozdíl mezi f-řetězci a t-řetězci, druhý demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/f-string-2.py |
| 19 | t-string-2.py | rozdíl mezi f-řetězci a t-řetězci, druhý demonstrační příklad | https://github.com/tisnik/most-popular-python-libs/blob/master/python3.14/t-string-2.py |
20. Odkazy na Internetu
- Python 3.14.0
https://test.python.org/downloads/release/python-3140/ - PEP 765 – Disallow return/break/continue that exit a finally block
https://peps.python.org/pep-0765/ - PEP 758 – Allow except and except* expressions without parentheses
https://peps.python.org/pep-0758/ - What’s new in Python 3.14 (official)
https://docs.python.org/3/whatsnew/3.14.html - What’s New In Python 3.13 (official)
https://docs.python.org/3/whatsnew/3.13.html - What’s New In Python 3.12 (official)
https://docs.python.org/3/whatsnew/3.12.html - What’s New In Python 3.11 (official)
https://docs.python.org/3/whatsnew/3.11.html - What’s New In Python 3.12
https://dev.to/mahiuddindev/python-312–4n43 - PEP 698 – Override Decorator for Static Typing
https://peps.python.org/pep-0698/ - PEP 484 – Type Hints
https://www.python.org/dev/peps/pep-0484/ - What’s New In Python 3.5
https://docs.python.org/3.5/whatsnew/3.5.html - 26.1. typing — Support for type hints
https://docs.python.org/3.5/library/typing.html#module-typing - Type Hints – Guido van Rossum – PyCon 2015 (youtube)
https://www.youtube.com/watch?v=2wDvzy6Hgxg - Python 3.5 is on its way
https://lwn.net/Articles/650904/ - Type hints
https://lwn.net/Articles/640359/ - Stránka projektu PDM
https://pdm.fming.dev/latest/ - PDF na GitHubu
https://github.com/pdm-project/pdm - PEP 582 – Python local packages directory
https://peps.python.org/pep-0582/ - PDM na PyPi
https://pypi.org/project/pdm/ - Which Python package manager should you use?
https://towardsdatascience.com/which-python-package-manager-should-you-use-d0fd0789a250 - How to Use PDM to Manage Python Dependencies without a Virtual Environment
https://www.youtube.com/watch?v=qOIWNSTYfcc - What are the best Python package managers?
https://www.slant.co/topics/2666/~best-python-package-managers - PEP 621 – Storing project metadata in pyproject.toml
https://peps.python.org/pep-0621/ - Pick a Python Lockfile and Improve Security
https://blog.phylum.io/pick-a-python-lockfile-and-improve-security/ - PyPA specifications
https://packaging.python.org/en/latest/specifications/ - Creation of virtual environments
https://docs.python.org/3/library/venv.html - How to Use virtualenv in Python
https://learnpython.com/blog/how-to-use-virtualenv-python/ - Python Virtual Environments: A Primer
https://realpython.com/python-virtual-environments-a-primer/ - virtualenv Cheatsheet
https://aaronlelevier.github.io/virtualenv-cheatsheet/ - Installing Python Modules
https://docs.python.org/3/installing/index.html - Python: The Documentary | An origin story
https://www.youtube.com/watch?v=GfH4QL4VqJ0 - History of Python
https://en.wikipedia.org/wiki/History_of_Python - History of Python
https://www.geeksforgeeks.org/python/history-of-python/ - IPython: jedno z nejpropracovanějších interaktivních prostředí pro práci s Pythonem
https://www.root.cz/clanky/ipython-jedno-z-nejpropracova-nejsich-interaktivnich-prostredi-pro-praci-s-pythonem/ - Další kulaté výročí v IT: dvacet let existence Pythonu 2
https://www.root.cz/clanky/dalsi-kulate-vyroci-v-it-dvacet-let-existence-pythonu-2/ - PEP 684 – A Per-Interpreter GIL
https://peps.python.org/pep-0684/ - What Is the Python Global Interpreter Lock (GIL)?
https://realpython.com/python-gil/ - PEP 703 – Making the Global Interpreter Lock Optional in CPython
https://peps.python.org/pep-0703/ - GlobalInterpreterLock
https://wiki.python.org/moin/GlobalInterpreterLock - What is the Python Global Interpreter Lock (GIL)
https://www.geeksforgeeks.org/what-is-the-python-global-interpreter-lock-gil/ - Let's remove the Global Interpreter Lock
https://www.pypy.org/posts/2017/08/lets-remove-global-interpreter-lock-748023554216649595.html - Global interpreter lock
https://en.wikipedia.org/wiki/Global_interpreter_lock - Rychlost CPythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači
https://www.root.cz/clanky/rychlost-cpythonu-3–11-a-3–12-v-porovnani-s-jit-a-aot-prekladaci-pythonu/ - Dokumentace Pythonu: balíček queue
https://docs.python.org/3/library/queue.html - Dokumentace Pythonu: balíček threading
https://docs.python.org/3/library/threading.html? - Dokumentace Pythonu: balíček multiprocessing
https://docs.python.org/3/library/multiprocessing.html - Dokumentace Pythonu: balíček asyncio
https://docs.python.org/3/library/asyncio.html - Synchronization Primitives
https://docs.python.org/3/library/asyncio-sync.html - Coroutines
https://docs.python.org/3/library/asyncio-task.html - Queues
https://docs.python.org/3/library/asyncio-queue.html - python-csp
https://python-csp.readthedocs.io/en/latest/ - TrellisSTM
http://peak.telecommunity.com/DevCenter/TrellisSTM - Python Multithreading and Multiprocessing Tutorial
https://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python




