Obsah
1. Nové vlastnosti Pythonu 3.14 v praxi: vliv odstranění GILu a využití více interpretrů
2. Překlad Pythonu 3.14 s vypnutým GILem
3. První benchmark: klasický neefektivní algoritmus bublinkového řazení
4. Porovnání rychlosti sekvenční varianty benchmarku pro různé verze Pythonu
5. Porovnání rychlosti paralelní varianty benchmarku pro Python 3.14 s GILem i bez GILu
6. Složitější výpočty s velkým množstvím numerických operací
7. Výsledky benchmarku pro různé verze Pythonu
8. Paralelní varianta výpočtu: využití proměnného počtu vláken
9. Výsledky benchmarku pro různé verze Pythonu
10. Rozdíly celkové doby výpočtů v Pythonu 3.14: GIL vs noGIL
11. Podpora pro větší množství interpretrů v běžícím procesu
12. Výpis interpretrů, konstrukce nového interpretru
13. Spuštění kódu v novém interpretru
14. Spuštění kódu v novém vláknu
15. Sdílí interpretry svůj stav?
16. Chování programu při spuštění dvou souběžných úloh v různých interpretrech
17. Chování programu v případě, že je v interpretru vyhozena nezachycená výjimka
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: vliv odstranění GILu a využití více interpretrů
Na článek Python 3.14: t-řetězce, barvičky, lepší nápověda a odcházející GIL, který byl vydán minulý týden, dnes navážeme. Zaměříme se na dvě technologie, které Python 3.14 nabízí. V první řadě se jedná o volitelné odstranění GILu (což sice není úplně žhavá novinka, ale v rámci Pythonu 3.14 došlo v této oblasti k různým vylepšením).
Ve druhé části článku si popíšeme API, které umožňuje využít větší množství interpretrů spouštěných v rámci jednoho procesu. Tyto interpretry jsou od sebe do značné míry izolovány, ale v případě potřeby mezi sebou mohou v nich spuštěné programy komunikovat přes fronty zpráv (message queue), což je metoda, která je v ekosystému jazyka Python používaná i v případě běžného multithreadingu a multiprocessingu.
2. Překlad Pythonu 3.14 s vypnutým GILem
Ještě před tím, než si vyzkoušíme benchmarky popsané v rámci dalších kapitol, je nutné si přeložit Python 3.14 ve verzi s GILem (běžná instalace) i taktéž bez GILu (budeme tedy mít dva samostatné interpretry). Průběh překladu 3.14 byl již popsán minule, takže jen velmi krátce:
Ze stránek www.python.org/downloads/ stáhnout archiv (zkomprimovaný tarball), ten rozbalit a použít klasickou kombinaci příkazů (je nutné mít nainstalován překladač céčka, včetně jeho sady nástrojů, a navíc i nástroj make):
$ cd Python-3.14.0 $ ./configure $ make
Výsledkem bude spustitelný interpret jazyka Python 3.14 s povoleným GILem.
Pro překlad varianty interpretru se zakázaným GILem je nutné příkaz configure volat s tímto parametrem:
$ ./configure --disable-gil
Tento příkaz vygeneruje novou variantu souboru Makefile:
... ... ... configure: creating ./config.status config.status: creating Makefile.pre config.status: creating Misc/python.pc config.status: creating Misc/python-embed.pc config.status: creating Misc/python-config.sh config.status: creating Modules/Setup.bootstrap config.status: creating Modules/Setup.stdlib config.status: creating Modules/ld_so_aix config.status: creating pyconfig.h config.status: pyconfig.h is unchanged configure: creating Modules/Setup.local configure: creating Makefile configure:
V nové variantě Makefile by se měly nově objevit tyto dva řádky:
# configure script arguments CONFIG_ARGS= '--disable-gil'
Další překlad již proběhne naprosto standardním způsobem:
$ make
Následováno příkazem:
$ sudo make install
Ověřme si, jakou variantu interpretru jsme vlastně přeložili a nainstalovali:
$ python3.14
Ve vypsané hlavičce by se měl objevit i nápis free-threading build, který u „běžného“ interpretru chybí:
Python 3.14.0 free-threading build (main, Oct 18 2025, 10:08:58) [GCC 14.2.1 20240912 (Red Hat 14.2.1-3)] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
3. První benchmark: klasický neefektivní algoritmus bublinkového řazení
Jako první benchmark pro zjištění výkonnosti interpretru Pythonu 3.14 byl zvolen algoritmus bublinkového řazení, který intenzivně pracuje s operační pamětí a v uvedené implementaci operuje s prvky seznamů. Samozřejmě se jedná o velmi neefektivní algoritmus (což vlastně benchmarky mají být :-). Čistě sekvenční varianta tohoto benchmarku, která žádným způsobem nevyužívá více threadů, vypadá následovně:
from time import perf_counter
import random
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
t1 = perf_counter()
for i in range(size - 1, 0, -1):
for j in range(0, i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
t2 = perf_counter()
print(f"Sorted in {t2-t1} seconds:")
t1 = perf_counter()
for i in range(100):
bubble_sort(5000)
t2 = perf_counter()
print(f"Total time: {t2-t1} seconds:")
Jak je z tohoto zdrojového kódu patrné, voláme v tomto benchmarku stokrát funkci, která vytvoří pole (realizované seznamem) se size prvky s náhodnou hodnotou (zde pro jednoduchost náhodné číslo v rozsahu 0..10000). Toto pole je následně seřazeno. Pro úplnost se ještě měří čas seřazení pole (provedeno stokrát pro různá pole) a taktéž celkový čas všech výpočtů.
Paralelní či možná lépe řečeno pseudoparalelní varianta benchmarku je prakticky stejná, ovšem oněch sto volání funkce bubble_sort není provedeno sekvenčně, ale využívá se namísto toho standardní třída nazvaná ThreadPoolExecutor. Hodnotou max_workers můžeme omezit počet úloh spouštěných současně:
from concurrent.futures.thread import ThreadPoolExecutor
from time import perf_counter
import random
def bubble_sort(size):
a = [random.randrange(0, 10000) for i in range(size)]
t1 = perf_counter()
for i in range(size - 1, 0, -1):
for j in range(0, i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
t2 = perf_counter()
print(f"Sorted in {t2-t1} seconds:")
t1 = perf_counter()
with ThreadPoolExecutor(max_workers=8) as executor:
for i in range(100):
executor.submit(bubble_sort, 5000)
t2 = perf_counter()
print(f"Total time: {t2-t1} seconds:")
4. Porovnání rychlosti sekvenční varianty benchmarku pro různé verze Pythonu
V následující tabulce jsou zobrazeny výsledky získané pro „sekvenční“ variantu benchmarku, tedy pro tu variantu, která nevyužívá spuštění výpočtů ve více vláknech. Benchmarky byly pochopitelně spuštěny na stejném počítači se stejně nastaveným režimem práce CPU (performance mode – to proto, že aby se CPU nezpomaloval, pokud se zahřeje):
| Verze Pythonu | Varianta | Celkový čas |
|---|---|---|
| 3.12 | GIL | 73s |
| 3.13 | GIL | 74s |
| 3.14 | GIL | 54s |
| 3.14 | noGIL | 66s |
Povšimněte si, že Python 3.14 je skutečně podstatně rychlejší, než předchozí dvě verze Pythonu (připomeňme, že výchozí konfigurace Pythonu 3.14 stále používá GIL). noGIL varianta Pythonu 3.14 je sice stále rychlejší, než předchozí verze Pythonu (s GILem), ovšem pomalejší, než Python 3.14 s GILem. Z pohledu tohoto benchmarku – sekvenční výpočty – se tedy noGIL verze nevyplatí.
Obrázek 1: Porovnání rychlosti sekvenční varianty benchmarku v grafické podobě. Zde jasně vyhrává Python 3.14 s GILem.
5. Porovnání rychlosti paralelní varianty benchmarku pro Python 3.14 s GILem i bez GILu
Zajímavější bude porovnání paralelní varianty benchmarku, protože zde by se měla projevit existence resp. naopak neexistence GILu. Nejprve porovnáme interpretry verze 3.12, 3.13 i 3.14, všechny s GILem:
| Verze Pythonu | Varianta | Celkový čas |
|---|---|---|
| 3.12 | GIL | 73s |
| 3.13 | GIL | 73s |
| 3.14 | GIL | 54s |
Zajímavé je, že časy dokončení benchmarku jsou prakticky stejné jako v případě jeho sekvenční varianty. Jinými slovy nám složitější zápis algoritmu nijak nepomohl, ovšem dobré je, že ani nijak neuškodil.
Obrázek 2: Porovnání rychlosti paralelní varianty benchmarku (interpretry s GIL) v grafické podobě. Opět vyhrává Python 3.14 s GILem.
Přejděme nyní na Python 3.14 bez GILu. Benchmark budeme spouštět pro různý počet workerů a tím pádem i pro různý počet vláken, které budou moci být provedeny souběžně:
| Počet vláken | Varianta | Celkový čas |
|---|---|---|
| 4 | noGIL | 18s |
| 8 | noGIL | 11s |
| 16 | noGIL | 9s |
Podle očekávání se čas běhu benchmarku postupně snižuje, což je ještě lépe patrné při pohledu na graf (pokud první hodnotu vynásobíme počtem vláken, dostaneme přibližně rychlost pro sekvenční variantu benchmarku):
Pro úplnost se podívejme na celkové časy běhu paralelní verze benchmarku pro počet vláken od 1 do 20:
Workers: 1 total time: 62.31223807000788 seconds Workers: 2 total time: 33.75166744997841 seconds Workers: 3 total time: 23.33235937001882 seconds Workers: 4 total time: 17.860626637993846 seconds Workers: 5 total time: 15.856282107997686 seconds Workers: 6 total time: 13.856884196982719 seconds Workers: 7 total time: 12.629000126005849 seconds Workers: 8 total time: 11.608698377996916 seconds Workers: 9 total time: 11.077733537007589 seconds Workers: 10 total time: 10.40789418600616 seconds Workers: 11 total time: 10.052095271006692 seconds Workers: 12 total time: 9.38940597101464 seconds Workers: 13 total time: 9.442261223011883 seconds Workers: 14 total time: 9.424980993004283 seconds Workers: 15 total time: 9.394032383017475 seconds Workers: 16 total time: 9.34619775399915 seconds Workers: 17 total time: 9.31484580898541 seconds Workers: 18 total time: 9.448251099995105 seconds Workers: 19 total time: 9.392468684003688 seconds
Na mém CPU by teoretické zrychlení mělo dosahovat osminásobku (CPU má osm skutečných jader, které se tváří jako šestnáct jader virtuálních), ovšem v praxi se v tomto případě nedosáhlo ani sedminásobku (i tak ovšem nejsou výsledky úplně špatné):
6. Složitější výpočty s velkým množstvím numerických operací
Větší počet jader mikroprocesoru, které mohou v ideálním případě pracovat paralelně, oceníme především ve chvíli, kdy je nutné vykonat nějaké složitější výpočty, neboť takové úlohy typicky nejsou omezeny rychlostí vstupně-výstupních operací a mnohdy ani neprovádějí velké množství čtení a zápisů do operační paměti. Jednou typicky výpočetně náročnou úlohou (kterou jsme v nepatrně pozměněné podobě již použili při testování nástroje Numba a mypyc), je výpočet nějakého fraktálu v komplexní rovině.
Obrázek 5: Jedna varianta fraktálu vykreslená benchmarkem.
Zkusme si tedy nechat vykreslit několik Juliových množin v nastaveném rozlišení (zde je konkrétně použito 256×256 pixelů) a s využitím většího maximálního počtu iterací (1000). Následující benchmark sekvenčně vypočítá několik desítek obrázků Juliových množin a uloží je na disk:
#!/usr/bin/env python
"""Renderer of the classic Julia fractal."""
import math
from time import perf_counter
from PIL import Image
IMAGE_WIDTH = 256
IMAGE_HEIGHT = 256
def julia(cx, cy, zx, zy, maxiter):
c = complex(cx, cy)
z = complex(zx, zy)
for i in range(maxiter):
if abs(z) > 2:
return i
z = z * z + c
return 0
def recalc_fractal(filename, palette, xmin, ymin, xmax, ymax, cx, cy, maxiter=1000):
"""Recalculate the whole fractal and render the set into given image."""
t1 = perf_counter()
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT))
width, height = image.size
stepx = (xmax - xmin) / width
stepy = (ymax - ymin) / height
y1 = ymin
for y in range(height):
x1 = xmin
for x in range(width):
i = julia(cx, cy, x1, y1, maxiter)
i = 3 * i % 256
color = (palette[i][0], palette[i][1], palette[i][2])
image.putpixel((x, y), color)
x1 += stepx
y1 += stepy
image.save(filename)
t2 = perf_counter()
# print("Done", filename, t2-t1)
def main():
import palette_mandmap
for angle in range(0, 360, 5):
rad = math.radians(angle)
cx = 1.0 * math.cos(rad)
cy = 1.0 * math.sin(rad)
filename = f"anim_{angle:03d}.png"
# print(filename)
recalc_fractal(filename, palette_mandmap.palette, -1.5, -1.5, 1.5, 1.5, cx, cy, 1000)
if __name__ == "__main__":
t1 = perf_counter()
main()
t2 = perf_counter()
print(f"Threads: no Rendering time: {t2-t1} seconds")
Obrázek 6: Další varianta fraktálu vykreslená benchmarkem.
7. Výsledky benchmarku pro různé verze Pythonu
V případě, že benchmark, jehož zdrojový kód byl uvedený v šesté kapitole, spustíme v různých verzích interpretru jazyka Python, zjistíme, že časy výpočtu jsou prakticky totožné. Python 3.14 je sice nepatrně rychlejší, než 3.12 nebo 3.13, ale rozdíly nejsou tak markantní, jako tomu bylo v případě benchmarku s bublinkovým řazením. To znamená, že počítaná smyčka a výpočty s komplexními čísly (tam benchmark stráví nejvíce času) se prakticky nijak neurychlily:
| Verze Pythonu | Varianta | Celkový čas |
|---|---|---|
| 3.12 | GIL | 8,7s |
| 3.13 | GIL | 8,5s |
| 3.14 | GIL | 8,4s |
Prakticky totožné časy výpočtu jsou dobře patrné i z grafické podoby výsledků:
8. Paralelní varianta výpočtu: využití proměnného počtu vláken
Výpočet z předchozích dvou kapitol je možné snadno převést do varianty, ve které se výpočet každého rastrového obrázku s Juliovou množinou odehraje v samostatném vláknu. V závislosti na konkrétní variantě interpretru Pythonu (s GILem či bez GILu) potom tato vlákna běží buď souběžně (concurrent) nebo paralelně (parallel). Počet vytvořených vláken lze specifikovat parametrem předávaným do konstruktoru ThreadPoolExecutor podobně, jako tomu bylo u benchmarku pro bublinkové řazení:
#!/usr/bin/env python
"""Renderer of the classic Julia fractal."""
import math
import sys
from concurrent.futures.thread import ThreadPoolExecutor
from time import perf_counter
from PIL import Image
IMAGE_WIDTH = 256
IMAGE_HEIGHT = 256
def julia(cx, cy, zx, zy, maxiter):
c = complex(cx, cy)
z = complex(zx, zy)
for i in range(maxiter):
if abs(z) > 2:
return i
z = z * z + c
return 0
def recalc_fractal(filename, palette, xmin, ymin, xmax, ymax, cx, cy, maxiter=1000):
"""Recalculate the whole fractal and render the set into given image."""
t1 = perf_counter()
image = Image.new("RGB", (IMAGE_WIDTH, IMAGE_HEIGHT))
width, height = image.size
stepx = (xmax - xmin) / width
stepy = (ymax - ymin) / height
y1 = ymin
for y in range(height):
x1 = xmin
for x in range(width):
i = julia(cx, cy, x1, y1, maxiter)
i = 3 * i % 256
color = (palette[i][0], palette[i][1], palette[i][2])
image.putpixel((x, y), color)
x1 += stepx
y1 += stepy
image.save(filename)
t2 = perf_counter()
# print("Done", filename, t2-t1)
def main(threads):
import palette_mandmap
with ThreadPoolExecutor(max_workers=threads) as executor:
for angle in range(0, 360, 5):
rad = math.radians(angle)
cx = 1.0 * math.cos(rad)
cy = 1.0 * math.sin(rad)
filename = f"anim_{angle:03d}.png"
# print(filename)
executor.submit(recalc_fractal, filename, palette_mandmap.palette, -1.5, -1.5, 1.5, 1.5, cx, cy, 1000)
if __name__ == "__main__":
threads = 8
if len(sys.argv) > 1:
threads = int(sys.argv[1])
t1 = perf_counter()
main(threads)
t2 = perf_counter()
print(f"Threads: {threads} Rendering time: {t2-t1} seconds")
9. Výsledky benchmarku pro různé verze Pythonu
Opět si ukažme výsledky pro různé verze interpretru programovacího jazyka Python. Použity byly interpretry s GILem (ostatně starší varianty Pythonu běh bez GILu neměly implementován):
| Verze Pythonu | Varianta | Celkový čas |
|---|---|---|
| 3.12 | GIL | 7,9s |
| 3.13 | GIL | 8,3s |
| 3.14 | GIL | 8,5s |
Zajímavé je, že v tomto případě vychází Python 3.14 vlastně nejhůře.
10. Rozdíly celkové doby výpočtů v Pythonu 3.14: GIL vs noGIL
Nyní se zaměřme pouze na interpret Pythonu 3.14, který – jak již víme – může být přeložen takovým způsobem, že nebude používat GIL. Benchmark spustíme ve verzi s GILem pro počet vláken, který se postupně mění od 1 do 16 (pro úplnost jsou na začátku vypsány i časy „sekvenční“ varianty benchmarku):
Threads: no Rendering time: 8.26075068900002 seconds Threads: 1 Rendering time: 8.611623036995297 seconds Threads: 2 Rendering time: 8.259407158999238 seconds Threads: 3 Rendering time: 8.441360425989842 seconds Threads: 4 Rendering time: 8.47209037898574 seconds Threads: 5 Rendering time: 8.53605182300089 seconds Threads: 6 Rendering time: 8.031249245000026 seconds Threads: 7 Rendering time: 8.194169141999964 seconds Threads: 8 Rendering time: 8.253845249000051 seconds Threads: 9 Rendering time: 8.439028205 seconds Threads: 10 Rendering time: 8.730016430999967 seconds Threads: 11 Rendering time: 8.830254810999918 seconds Threads: 12 Rendering time: 8.628374089999966 seconds Threads: 13 Rendering time: 8.723840595999945 seconds Threads: 14 Rendering time: 8.796107125000049 seconds Threads: 15 Rendering time: 8.895810613000094 seconds Threads: 16 Rendering time: 8.817675302999987 seconds
Naprosto odlišná však bude situace ve chvíli, kdy použijeme Python 3.14 se zakázaným GILem. Opět si ukažme výsledky pro benchmark rozdělený do jednoho až šestnácti vláken:
Threads: 1 Rendering time: 9.435709450015565 seconds Threads: 2 Rendering time: 5.053133198001888 seconds Threads: 3 Rendering time: 3.562595502997283 seconds Threads: 4 Rendering time: 2.833458012988558 seconds Threads: 5 Rendering time: 2.4556286619917955 seconds Threads: 6 Rendering time: 2.1379492330015637 seconds Threads: 7 Rendering time: 1.9770731370081194 seconds Threads: 8 Rendering time: 1.872884926997358 seconds Threads: 9 Rendering time: 1.864864025003044 seconds Threads: 10 Rendering time: 1.7349536379915662 seconds Threads: 11 Rendering time: 1.734476473997347 seconds Threads: 12 Rendering time: 1.9110816899919882 seconds Threads: 13 Rendering time: 1.775416452990612 seconds Threads: 14 Rendering time: 1.7973549320013262 seconds Threads: 15 Rendering time: 1.8662592969776597 seconds Threads: 16 Rendering time: 1.843843137001386 seconds
Teoreticky by se mělo pro počet vláken přesahujících hodnotu 8 dosáhnout osminásobného urychlení (což je počet reálných jader), ve skutečnosti však urychlení dosáhne jen 5,5 násobku původního výpočtu. To nejsou zcela špatné výsledky, ovšem na druhou stranu by mohly být i lepší a naznačuje to existenci zámků buď přímo v interpretru nebo v použité knihovně:
11. Podpora pro větší množství interpretrů v běžícím procesu
V Pythonu 3.14 je k dispozici nová standardní knihovna concurrent.interpreters. Jak již název této knihovny naznačuje, poskytuje možnost konstrukce nových instancí interpretru Pythonu, spuštění kódu v těchto interpretrech a taktéž komunikaci mezi programy, které běží v jednotlivých interpretrech a jsou od sebe izolovány (což znamená, že obecně nesdílí svoji paměť). V názvu této knihovny je i slovo concurrent, protože jednotlivé interpretry běží souběžně (a v některých případech i paralelně, což je silnější podmínka, než souběžnost).
12. Výpis interpretrů, konstrukce nového interpretru
Práce s interpretry na úrovni zdrojového kódu je relativně snadná a přímočará. Nejprve se podívejme na to, jakým způsobem se vlastně získá seznam všech dostupných interpretrů (ty mohou, ale také nemusí běžet, jsou však již zkonstruovány):
from concurrent import interpreters
for interpreter in interpreters.list_all():
print(interpreter)
Při inicializaci Pythonu 3.14 je zkonstruován jen jediný interpret. Ten je tímto skriptem vypsán (resp. je vypsáno jméno objektu reprezentujícího rozhraní k interpretru):
Interpreter(0)
Nový interpret se zkonstruuje funkcí interpreters.create. I když není v tomto interpretru spuštěn žádný kód, bude i tento interpretr vypsán:
from concurrent import interpreters
interp = interpreters.create()
for interpreter in interpreters.list_all():
print(interpreter)
Výsledky:
Interpreter(0) Interpreter(1)
13. Spuštění kódu v novém interpretru
V nově vytvořeném interpretru je pochopitelně možné spustit nějaký kód. Pro tento účel slouží metody call a exec. Ukažme si použití metody call, která akceptuje identifikátor funkce a její případné parametry. Funkce je spuštěna „izolovaně“ v novém interpretru:
from concurrent import interpreters
interp = interpreters.create()
def run():
print("Hello from new interpreter")
print("Executing run()")
interp.call(run)
print("Hello from the original interpreter")
Výsledky:
Executing run() Hello from new interpreter Hello from the original interpreter
Přičemž prostřední zpráva je vypsána kódem funkce run běžící v novém interpretru.
Podobně lze použít i exec:
from concurrent import interpreters
interp = interpreters.create()
def run():
print("Hello from new interpreter")
print("Executing run()")
interp.exec(run)
print("Hello from the original interpreter")
Metodou exec je možné spustit i externí skript, kdežto metoda call je omezena na zavolání funkcí:
from concurrent import interpreters
interp = interpreters.create()
print("Executing run()")
interp.exec(open("hello.py").read())
print("Hello from the original interpreter")
14. Spuštění kódu v novém vláknu
Ve světě mikroprocesorů s velkým množstvím procesorových jader je velmi užitečné, že interpret může spustit zadanou funkci v novém vláknu. To je ukázáno na dalším demonstračním příkladu, v němž funkci run spustíme nejenom v novém interpretru, ale navíc i v novém vláknu, které může běžet souběžně s původním vláknem:
from concurrent import interpreters
interp = interpreters.create()
def run():
print("Hello from new interpreter")
print("Executing run()")
t = interp.call_in_thread(run)
print("Hello from original interpreter")
t.join()
V tomto demonstračním příkladu navíc explicitně čekáme na dokončení vlákna, ve kterém byla funkce run spuštěna:
Executing run() Hello from original interpreter Hello from new interpreter
15. Sdílí interpretry svůj stav?
V předchozích odstavcích bylo napsáno, že funkce (či jiný kód) běžící v jednotlivých interpretrech je odizolován od kódu z jiných interpretrů. Zda tomu tak je se můžeme pokusit zjistit různými způsoby. Ukažme si ten nejjednodušší z nich. Ve funkci run se pokusíme o modifikaci obsahu globální proměnné:
from concurrent import interpreters
interp = interpreters.create()
x = []
def run():
x.append("*")
print("x=", x)
run()
print("x=", x)
t = interp.call_in_thread(run)
t.join()
print("x=", x)
Spuštění tohoto skriptu povede k běhové chybě:
x= []
x= ['*']
x= []
x= ['*']
Exception in thread Thread-1 (_call):
Exception: ModuleNotFoundError: No module named '<fake __main__>'
The above exception was the direct cause of the following exception:
concurrent.interpreters.NotShareableError: object could not be unpickled
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/local/lib/python3.14t/threading.py", line 1081, in _bootstrap_inner
self._context.run(self.run)
~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/usr/local/lib/python3.14t/threading.py", line 1023, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14t/concurrent/interpreters/__init__.py", line 215, in _call
res, excinfo = _interpreters.call(self._id, callable, args, kwargs, restrict=True)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
concurrent.interpreters.NotShareableError: func not shareable
To ovšem neznamená, že kód v různých interpretrech nedokáže komunikovat s jiným kódem. Komunikace možná je, ovšem přes fronty, což si taktéž příště ukážeme.
16. Chování programu při spuštění dvou souběžných úloh v různých interpretrech
Ještě jednou se vraťme k problematice spuštění kódu jak v samostatném interpretru, tak i v samostatném vláknu. Budeme zkoumat, jak se takový program bude chovat v čase běhu, tedy v runtime. Nejprve nepatrně upravíme skript, v němž se funkce run spouští „jen“ v novém interpretru, ale pořád ve stejném vlákně. Přidáme volání funkce sleep:
from time import sleep
from concurrent import interpreters
interp = interpreters.create()
def run():
from time import sleep
sleep(5)
print("Hello from new interpreter")
print("Executing run()")
interp.exec(run)
sleep(5)
print("Hello from the original interpreter")
Podle očekávání se zprávy zobrazí s přibližně pětisekundovými prodlevami:
Executing run() <5 sekund> Hello from new interpreter <5 sekund> Hello from the original interpreter
Jak se ovšem chování změní tehdy, pokud bude run spuštěna v novém vláknu?
from time import sleep
from concurrent import interpreters
interp = interpreters.create()
def run():
from time import sleep
sleep(5)
print("Hello from new interpreter")
print("Executing run()")
t = interp.call_in_thread(run)
sleep(5)
print("Hello from original interpreter")
t.join()
Druhé dvě zprávy se zobrazí v přibližně stejný okamžik, protože je sice před nimi pětisekundová prodleva, ta však byla vykonána souběžně jak v původním vláknu, tak i ve vláknu novém:
Executing run() <5 sekund> Hello from original interpreter Hello from new interpreter
17. Chování programu v případě, že je v interpretru vyhozena nezachycená výjimka
Zajímavé bude zjistit, jak se bude celý program chovat ve chvíli, kdy je z funkce spuštěné v novém (samostatném) interpretru vyhozena výjimka, která není odchycena. Může se jednat například o chybu při dělení atd., tj. o takový typ výjimky, který se (většinou) přímo neošetřuje. Nejprve spustíme kód v novém interpretru s využitím metody exec, tj. vše poběží v jediném vláknu:
from concurrent import interpreters
interp = interpreters.create()
def run():
print("Hello from new interpreter")
0/0
print("Executing run()")
interp.exec(run)
print("Hello from the original interpreter")
Pokusme se tento skript spustit a sledovat, jaké operace se vykonají:
$ python3.14 interpreter_exception_1.py
Executing run()
Hello from new interpreter
Traceback (most recent call last):
File "/home/ptisnovs/src/most-popular-python-libs/python3.14/interpreters/interpreter_exception_1.py", line 10, in
interp.exec(run)
~~~~~~~~~~~^^^^^
File "/usr/local/lib/python3.14/concurrent/interpreters/__init__.py", line 212, in exec
raise ExecutionFailed(excinfo)
concurrent.interpreters.ExecutionFailed: ZeroDivisionError: division by zero
Uncaught in the interpreter:
Traceback (most recent call last):
File "/home/ptisnovs/src/most-popular-python-libs/python3.14/interpreters/interpreter_exception_1.py", line 7, in run
0/0
~^~
ZeroDivisionError: division by zero
V tomto případě došlo k „běžnému“ pádu celé aplikace, což znamená, že interpretry jsou sice od sebe izolovány, ale vlastní volání funkce v interpretru může zhavarovat pokud dojde k vyhození výjimky.
Spuštění kódu v novém vláknu (a v novém interpretru) se bude chovat odlišně, i když dojde k vyhození stejné výjimky:
from time import sleep
from concurrent import interpreters
interp = interpreters.create()
def run():
print("Hello from new interpreter")
0/0
print("Executing run()")
t = interp.call_in_thread(run)
print("Hello from the original interpreter")
sleep(5)
print("Original interpreter is still alive")
t.join()
Nyní je výjimkou ukončen běh kódu v jiném vláknu, ovšem původní interpret stále poběží, což je patrné z hlášení, která jsou zobrazena:
$ python3.14 interpreter_exception_2.py
Executing run()
Hello from the original interpreter
Hello from new interpreter
Exception in thread Thread-1 (_call):
Traceback (most recent call last):
File "/usr/local/lib/python3.14/threading.py", line 1081, in _bootstrap_inner
self._context.run(self.run)
~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/usr/local/lib/python3.14/threading.py", line 1023, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/concurrent/interpreters/__init__.py", line 217, in _call
raise ExecutionFailed(excinfo)
concurrent.interpreters.ExecutionFailed: ZeroDivisionError: division by zero
Uncaught in the interpreter:
Traceback (most recent call last):
File "/home/ptisnovs/src/most-popular-python-libs/python3.14/interpreters/interpreter_exception_2.py", line 8, in run
0/0
~^~
ZeroDivisionError: division by zero
Original interpreter is still alive
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, paralelnosti a asynchronního běhu 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 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/
19. Repositář s demonstračními příklady
Všechny dnes 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 minulé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/



