Obsah
1. Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio
2. Spuštění korutiny v knihovnách Curio a Trio
3. Předání parametrů synchronně či asynchronně volané korutině
4. Chování programu při spuštění několika korutin funkcí trio.run
5. Asynchronní spuštění korutin v knihovně curio
6. „Strukturované programování souběžných úloh“
7. Spuštění korutin s čekáním na jejich dokončení v bloku async with
8. Postupné spouštění korutin ve více blocích async with
9. Výjimky vznikající v korutinách
10. Zachycení výjimek vyhazovaných z korutin
11. Vznik výjimek souběžně v několika korutinách
12. Je počet korutin (souběžných úloh) prakticky omezen?
13. Paměťové nároky programu s 10000 korutinami
14. Spuštění 10000 souběžných úloh se sledováním vytížení CPU
15. Komunikace mezi souběžnými úlohami s využitím kanálů
16. Základní vlastnosti kanálů nabízených knihovnou Trio
17. Ukázka klasické úlohy typu producent-konzument
19. Repositář s demonstračními příklady
1. Souběžné a paralelně běžící úlohy naprogramované v Pythonu – knihovna Trio
Podobný koncept práce s korutinami, který jsme viděli v knihovně Curio v předchozím článku, je implementován i v knihovně nazvané Trio, která z Curia částečně vychází. Cílem této knihovny je nejenom nabídnout programátorům souběžný běh korutin (a plně paralelní běh I/O operací – což je úkol pro operační systém), ale navíc i zajistit korektnost výsledného programu, což vůbec není jednoduchý úkol. Podrobnější informace o této velmi užitečné knihovně si řekneme v navazujících kapitolách.
Autor knihovny Trio je velkým zastáncem metodiky strukturovaného programování souběžných úloh. Při vysvětlování svého přístupu se vrací do doby, kdy byly programy (například ve Fortranu a BASICu) tvořeny nestrukturovaným kódem plným příkazů goto. Takový kód nebyl příliš přehledný a navíc možnost „skoku kamkoli“ mnohdy vedla k jeho chybnému chování. Problematika byla vyřešena zavedením strukturovaných konstrukcí – procedur, podmínek a smyček (což nutně nemusí vést k zavedení nových příkazů do jazyka, spíše se jedná o návrhové vzory). Podobná situace (podle autora Tria) vládne v oblasti souběžných úloh, kdy se například příkazem go spouští gorutiny zcela nekoordinovaně a kdekoli; navíc není zaručeno jejich ukončení ani zachycení případných chyb. Namísto takto nestrukturovaného přístupu je v Triu použit blok async with popř. async for, což si ukážeme na demonstračních příkladech.
2. Spuštění korutiny v knihovnách Curio a Trio
V předchozím článku jsme si ukázali způsob spuštění korutiny z hlavní funkce (main). Korutina je reprezentována funkcí s deklarací async a nelze ji tedy spustit přímo. Proto je nutné (v případě použití knihovny Curio popsané minule) použít funkci nazvanou run. Povšimněte si, že prvním (a jediným povinným) parametrem této funkce je korutina, která se má spustit. To mj. znamená, že se můžeme obejít bez explicitního vytvoření úlohy a jejího následného volání:
import curio async def task(): print("task started") await curio.sleep(5) print("task finished") def main(): print("main started") curio.run(task) print("done") main()
Prakticky stejným způsobem bude problematika spuštění korutiny realizována v knihovně Trio. Oba příklady jsou prakticky totožné, liší se pouze odlišným importem a jiným jménem balíčku s volanou funkcí run:
import trio async def task(): print("task started") await trio.sleep(5) print("task finished") def main(): print("main started") trio.run(task) print("done") main()
Povšimněte si však situace, kdy v korutině task zavoláme korutinu trio.sleep přímo, tedy bez použití klíčového slova await. To je obecně nekorektní řešení:
import trio async def task(): print("task started") trio.sleep(5) print("task finished") def main(): print("main started") trio.run(task) print("done") main()
Na tento problém nás knihovna Trio upozorní varováním (a toto varování je vhodné ihned opravit):
main started task started trio_01_error.py:6: RuntimeWarning: coroutine 'sleep' was never awaited trio.sleep(5) RuntimeWarning: Enable tracemalloc to get the object allocation traceback task finished done
3. Předání parametrů synchronně či asynchronně volané korutině
Korutinám je v naprosté většině případů nutné předávat nějaké parametry. V případě, že je použita knihovna trio, jsou parametry korutině předány přímo v rámci funkce trio.run. Povšimněte si podstatného rozdílu – nevoláme zde přímo korutinu task (ve skutečnosti ani v případě asyncio nedochází k jejímu přímému volání), ale reference na (synchronně či asynchronně) volanou korutinu se společně s jejími parametry předává funkci run:
import trio async def task(n, s): print("task started") for i in range(n): print(f"{i+1}/{n}") await trio.sleep(s) print("task finished") def main(): print("main started") trio.run(task, 10, 1) print("done") main()
Po spuštění tohoto příkladu uvidíme, že se díky await vždy čeká na dokončení předchozí korutiny:
main started task started 1/10 2/10 3/10 4/10 5/10 6/10 7/10 8/10 9/10 10/10 task finished done
4. Chování programu při spuštění několika korutin funkcí trio.run
Zkusme si nyní spustit následující skript, v němž jsou vytvořeny a spuštěny celkem tři korutiny. Varianta určená pro knihovnu curie vypadá takto (viz též předchozí článek):
import curio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await curio.sleep(s) print(f"{name} task finished") def main(): print("main started") curio.run(task, "1st", 10, 1) curio.run(task, "2nd", 10, 1) curio.run(task, "3rd", 10, 1) print("done") main()
Prakticky stejným způsobem lze realizovat tentýž skript, nyní ovšem s využitím knihovny Trio:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") def main(): print("main started") trio.run(task, "1st", 10, 1) trio.run(task, "2nd", 10, 1) trio.run(task, "3rd", 10, 1) print("done") main()
Po spuštění tohoto skriptu získáme následující výstup, který jasně ukazuje, že i přes volání await trio.sleep v korutině nedojde k přepnutí na další korutinu – další korutina totiž ještě ani nebyla vytvořena. To vlastně znamená, že trio.run zajišťuje synchronní volání korutiny (protože se skutečně jedná o korutinu získanou transformací funkce s využitím konstrukce async):
main started 1st task started 1st 1/10 1st 2/10 1st 3/10 1st 4/10 1st 5/10 1st 6/10 1st 7/10 1st 8/10 1st 9/10 1st 10/10 1st task finished 2nd task started ... ... ... 3rd task started 3rd 1/10 3rd 2/10 3rd 3/10 3rd 4/10 3rd 5/10 3rd 6/10 3rd 7/10 3rd 8/10 3rd 9/10 3rd 10/10 3rd task finished done
5. Asynchronní spuštění korutin v knihovně curio
Pro skutečné asynchronní spouštění korutin se v knihovně curio používá funkce spawn. Knihovna trio sice programátorům nabízí jiné (lepší) řešení, ovšem pro porovnání si nejdříve ukažme, jak lze spustit tři korutiny s využitím možností nabízených knihovnou curio:
import curio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await curio.sleep(s) print(f"{name} task finished") async def main(): print("main started") task1 = await curio.spawn(task, "1st", 10, 1) task2 = await curio.spawn(task, "2nd", 10, 1) task3 = await curio.spawn(task, "3rd", 10, 1) await task1.join() await task2.join() await task3.join() print("done") curio.run(main())
Nyní dojde ke skutečnému souběžnému běhu korutin, mezi nimiž se přepíná během „spánku“ vyvolanému zavoláním curio.sleep:
main started 1st task started 1st 1/10 2nd task started 2nd 1/10 3rd task started 3rd 1/10 1st 2/10 2nd 2/10 3rd 2/10 1st 3/10 2nd 3/10 3rd 3/10 1st 4/10 2nd 4/10 3rd 4/10 1st 5/10 2nd 5/10 3rd 5/10 1st 6/10 2nd 6/10 3rd 6/10 1st 7/10 2nd 7/10 3rd 7/10 1st 8/10 2nd 8/10 3rd 8/10 1st 9/10 2nd 9/10 3rd 9/10 1st 10/10 2nd 10/10 3rd 10/10 1st task finished 2nd task finished 3rd task finished done
6. „Strukturované programování souběžných úloh“
Knihovna Trio je z pohledu programátora-uživatele do značné míry postavena na bloku async with, jenž vychází z klasického bloku with. Jedná se o velmi důležitou součást jazyka Python, protože umožňuje čitelně zapsat několik idiomů. Jedním z těchto idiomů je používání správců kontextu s využitím bloků with pro ty prostředky, které se mají automaticky uzavírat po odchodu z bloku with (a to jakýmkoli způsobem, včetně výskoku z funkce atd.).
Klasickým příkladem je otevření souboru s tím, že jeho uzavření je provedeno automaticky po opuštění bloku with (a to jakýmkoli způsobem, tedy i vyhozením výjimky):
with open('hello.txt', 'w') as fout: fout.write('Hi there!')
Interně je funkcionalita bloku with založena na správcích kontextu. V praxi to znamená, že pokud nějaká třída implementuje dvě speciální metody __enter__ a __exit__ nezbytné pro to, aby se jednalo o korektně naprogramované správce kontextu (context manager), je možné ji použít v bloku with:
class Context(): def __init__(self): print("Context: init") def __enter__(self): print("Context: enter") return "foo" def __exit__(self, type, value, traceback): print("Context: exit", type, value, traceback) print("Before with block") with Context() as c: print("Inside with block") print(c) print("After with block")
Chování po spuštění ukazuje volání obou speciálních metod:
Before with block Context: init Context: enter Inside with block foo Context: exit None None None After with block
V souvislosti s podporou asynchronního programování byl do Pythonu přidán i blok async with, který pro správce kontextu vyžaduje implementaci speciálních metod __aenter__ a __aexit__:
import trio class AsyncContext(): def __init__(self): print("Context: init") def __aenter__(self): print("Context: aenter") return trio.sleep(2) def __aexit__(self, type, value, traceback): print("Context: aexit", type, value, traceback) return self def __await__(self): print("Context: await") return None async def main(): print("Before with block") async with AsyncContext() as c: print("Inside with block") print(c) print("After with block") trio.run(main)
A právě blok async with bude použit i ve všech dalších demonstračních příkladech.
7. Spuštění korutin s čekáním na jejich dokončení v bloku async with
Namísto spawn a join se v knihovně Trio používá blok async with se správcem kontextu nazvaným „nursery“. To je přiléhavé jméno, protože se v tomto bloku vytváří „děti–korutiny“ a správce kontextu se stará o jejich sledování a správu. V následujícím příkladu jsou vytvořeny a spuštěny tři korutiny a před opuštěním bloku async with se čeká na jejich dokončení:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: nursery.start_soon(task, "1st", 10, 1) nursery.start_soon(task, "2nd", 10, 1) nursery.start_soon(task, "3rd", 10, 1) print("done") trio.run(main)
Přesvědčme se o tom, že korutiny běží souběžně:
main started 3rd task started 3rd 1/10 2nd task started 2nd 1/10 1st task started 1st 1/10 3rd 2/10 2nd 2/10 1st 2/10 1st 3/10 2nd 3/10 3rd 3/10 1st 4/10 2nd 4/10 3rd 4/10 1st 5/10 2nd 5/10 3rd 5/10 3rd 6/10 2nd 6/10 1st 6/10 1st 7/10 2nd 7/10 3rd 7/10 3rd 8/10 2nd 8/10 1st 8/10 3rd 9/10 2nd 9/10 1st 9/10 3rd 10/10 2nd 10/10 1st 10/10 1st task finished 2nd task finished 3rd task finished done
Po spuštění korutiny se nevrací žádná hodnota, tedy ani žádná future atd.:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: print(nursery.start_soon(task, "1st", 10, 1)) print(nursery.start_soon(task, "2nd", 10, 1)) print(nursery.start_soon(task, "3rd", 10, 1)) print("done") trio.run(main)
Povšimněte si první trojice hodnot None:
main started None None None 1st task started 1st 1/10 2nd task started 2nd 1/10 3rd task started 3rd 1/10 1st 2/10 2nd 2/10 3rd 2/10 1st 3/10 2nd 3/10 3rd 3/10 1st 4/10 2nd 4/10 3rd 4/10 3rd 5/10 2nd 5/10 1st 5/10 1st 6/10 2nd 6/10 3rd 6/10 1st 7/10 2nd 7/10 3rd 7/10 3rd 8/10 2nd 8/10 1st 8/10 1st 9/10 2nd 9/10 3rd 9/10 3rd 10/10 2nd 10/10 1st 10/10 3rd task finished 2nd task finished 1st task finished done
8. Postupné spouštění korutin ve více blocích async with
Pokud se mají korutiny spustit postupně, je nutné použít více bloků async with nebo přímo await (což je kratší):
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: nursery.start_soon(task, "1st", 10, 1) async with trio.open_nursery() as nursery: nursery.start_soon(task, "2nd", 10, 1) async with trio.open_nursery() as nursery: nursery.start_soon(task, "3rd", 10, 1) print("done") trio.run(main)
Nyní se bude program chovat odlišně:
main started 1st task started 1st 1/10 1st 2/10 1st 3/10 1st 4/10 1st 5/10 ... ... ... 1st 10/10 1st task finished 2nd task started 2nd 1/10 2nd 2/10 ... ... ... 2nd 9/10 2nd 10/10 2nd task finished 3rd task started 3rd 1/10 ... ... ... 3rd 9/10 3rd 10/10 3rd task finished done
9. Výjimky vznikající v korutinách
V korutině, ostatně stejně jako v jakékoli jiné části kódu, může vzniknout výjimka. Ta může být zachycena kódem, který korutinu vytvořil, a to na přesně známém místě – díky existenci bloku async with. Podívejme se na velmi jednoduchou korutinu, která před svým ukončením vyvolá výjimku:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) raise Exception(name) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: nursery.start_soon(task, "1st", 10, 0.3) nursery.start_soon(task, "2nd", 10, 0.3) nursery.start_soon(task, "3rd", 10, 0.3) print("done") trio.run(main)
Tato výjimka není nikde zachycena a proto „probublá“ až do kódu, který náš program spustil:
main started 1st task started 1st 1/10 2nd task started 2nd 1/10 3rd task started 3rd 1/10 1st 2/10 2nd 2/10 3rd 2/10 3rd 3/10 2nd 3/10 1st 3/10 3rd 4/10 2nd 4/10 1st 4/10 3rd 5/10 2nd 5/10 1st 5/10 3rd 6/10 2nd 6/10 1st 6/10 1st 7/10 2nd 7/10 3rd 7/10 1st 8/10 2nd 8/10 3rd 8/10 1st 9/10 2nd 9/10 3rd 9/10 1st 10/10 2nd 10/10 3rd 10/10 Traceback (most recent call last): File "trio_07.py", line 24, in <Module> trio.run(main) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 1946, in run raise runner.main_task_outcome.error File "trio_07.py", line 20, in main nursery.start_soon(task, "3rd", 10, 0.3) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 813, in __aexit__ raise combined_error_from_nursery File "trio_07.py", line 11, in task raise Exception(name) Exception: 3rd
10. Zachycení výjimek vyhazovaných z korutin
Výjimku vyhozenou z korutiny lze zachytit v programovém kódu, který korutinu vytvořil a spustil (tedy buď přímo uvnitř nebo vně bloku async with). Tento koncept je ukázán na následujícím příkladu:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) raise Exception(name) print(f"{name} task finished") async def main(): print("main started") try: async with trio.open_nursery() as nursery: nursery.start_soon(task, "1st", 10, 0.3) nursery.start_soon(task, "2nd", 10, 0.3) nursery.start_soon(task, "3rd", 10, 0.3) except Exception as e: print("Caught", e) print("done") trio.run(main)
Zde výjimka vede k ukončení bloku async with a tím pádem i k ukončení korutin:
main started 3rd task started 3rd 1/10 2nd task started 2nd 1/10 1st task started 1st 1/10 1st 2/10 2nd 2/10 3rd 2/10 3rd 3/10 2nd 3/10 1st 3/10 1st 4/10 2nd 4/10 3rd 4/10 1st 5/10 2nd 5/10 3rd 5/10 3rd 6/10 2nd 6/10 1st 6/10 3rd 7/10 2nd 7/10 1st 7/10 1st 8/10 2nd 8/10 3rd 8/10 1st 9/10 2nd 9/10 3rd 9/10 3rd 10/10 2nd 10/10 1st 10/10 Caught 3rd done
Ještě lépe bude chování patrné v situaci, kdy jedna z korutin skončí (s výjimkou) dříve, než korutiny další:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) raise Exception(name) print(f"{name} task finished") async def main(): print("main started") try: async with trio.open_nursery() as nursery: nursery.start_soon(task, "1st", 10, 1.0) nursery.start_soon(task, "2nd", 10, 1.0) nursery.start_soon(task, "3rd", 10, 0.1) except Exception as e: print("Caught", e) print("done") trio.run(main)
Z výpisu je patrné, že výjimka způsobí ukončení dalších korutin:
main started 3rd task started 3rd 1/10 2nd task started 2nd 1/10 1st task started 1st 1/10 3rd 2/10 3rd 3/10 3rd 4/10 3rd 5/10 3rd 6/10 3rd 7/10 3rd 8/10 3rd 9/10 3rd 10/10 2nd 2/10 1st 2/10 Caught 3rd done
11. Vznik výjimek souběžně v několika korutinách
V praxi může nastat situace, kdy vznikne několik výjimek v souběžně běžících korutinách. Jak je však možné na tyto výjimky reagovat, když celý koncept výjimek počítá se vznikem jediné výjimky? Knihovna Trio poskytuje velmi elegantní řešení. Podívejme se nejprve na příklad v němž vzniknou tři výjimky, každá jiného typu:
import trio async def task1(): dct = {} return dct["foo"] async def task2(): l = [] return l[10] async def task3(): x = 0 return 1/x async def main(): print("main started") async with trio.open_nursery() as nursery: nursery.start_soon(task1) nursery.start_soon(task2) nursery.start_soon(task3) print("done") trio.run(main)
Vidíme, že každá korutina skutečně vyhodí výjimku, pokaždé jiného typu. Po spuštění příkladu by tedy mělo být možné nějakým způsobem všechny tyto výjimky zpracovat:
main started Traceback (most recent call last): File "trio_10.py", line 28, in trio.run(main) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 1946, in run raise runner.main_task_outcome.error File "trio_10.py", line 24, in main nursery.start_soon(task3) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 813, in __aexit__ raise combined_error_from_nursery trio.MultiError: ZeroDivisionError('division by zero'), IndexError('list index out of range'), KeyError('foo') Details of embedded exception 1: Traceback (most recent call last): File "trio_10.py", line 16, in task3 return 1/x ZeroDivisionError: division by zero Details of embedded exception 2: Traceback (most recent call last): File "trio_10.py", line 11, in task2 return l[10] IndexError: list index out of range Details of embedded exception 3: Traceback (most recent call last): File "trio_10.py", line 6, in task1 return dct["foo"] KeyError: 'foo'
Vidíme, že byla vyhozena jediná výjimka typu MultiError, která však obsahuje přesné informace o všech výjimkách, které v korutinách skutečně vznikly.
12. Je počet korutin (souběžných úloh) prakticky omezen?
Počet současně spuštěných procesů bývá omezen praktickými možnostmi plánovače operačního systému. V podstatě totéž je možné říci o vláknech, jejichž využití je zejména v Pythonu navíc omezeno i existencí GILu. Jak je však tomu v případě souběžných úloh neboli korutin? Ty jsou – co se týče paralelního běhu – taktéž omezeny GILem, zajímavé ovšem bude taktéž ověřit, jaké jsou paměťové nároky korutin popř. do jaké míry existují omezení v oblasti jejich plánování.
V následujícím příkladu je spuštěno 100 korutin, které poběží souběžně po dobu 100 sekund (což je dostatečný čas na provedení měření):
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: for i in range(100): nursery.start_soon(task, f"Task {i}", 1, 100) print("done") trio.run(main)
Po spuštění tohoto programu zjistíme číslo odpovídajícího procesu:
$ ps ax |grep python 614 ? Ss 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers 4410 ? S 0:59 /usr/bin/python3 /usr/share/system-config-printer/applet.py 4428 ? S 0:00 python3 /usr/lib/blueberry/safechild /usr/sbin/rfkill event 3049128 pts/2 Ss+ 0:00 python3 trio_11.py 3049130 pts/3 S+ 0:00 grep --color=auto python
A následně si necháme vypsat jeho paměťové nároky:
$ pmap 3049128 3049128: python3 trio_11.py 00007f74803bc000 4K rw--- [ anon ] 00007ffec21cb000 132K rw--- [ stack ] 00007ffec21f4000 12K r---- [ anon ] 00007ffec21f7000 4K r-x-- [ anon ] ffffffffff600000 4K --x-- [ anon ] total 31772K
V rámci další kapitoly pak počet procesů zvýšíme na 10000, což už je číslo, které je v případě použití procesů či vláken velmi problematické.
13. Paměťové nároky programu s 10000 korutinami
Podívejme se nyní na paměťové nároky programu, který namísto pouhých sto korutin vytvoří 10000 korutin, jenž budou opravdu v daný okamžik existovat „souběžně“. Každá korutina totiž bude volat korutinu trio.sleep, přičemž čas čekání bude nastaven na 10000 sekund, což je dostatečně dlouhá doba na prozkoumání paměťových nároků takového programu:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: for i in range(10000): nursery.start_soon(task, f"Task {i}", 1, 10000) print("done") trio.run(main)
Po spuštění tohoto programu nejdříve zjistíme odpovídající číslo procesu (PID):
$ ps ax |grep python 614 ? Ss 0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers 4410 ? S 0:59 /usr/bin/python3 /usr/share/system-config-printer/applet.py 4428 ? S 0:00 python3 /usr/lib/blueberry/safechild /usr/sbin/rfkill event 3049130 pts/2 Ss+ 0:00 python3 trio_11.py 3049132 pts/3 S+ 0:00 grep --color=auto python
A následně si necháme vypsat paměťové nároky tohoto procesu:
$ pmap 3049130 3049128: python3 trio_11.py 00007fa95aed5000 28K r--s- gconv-modules.cache 00007fa95aedc000 4K r---- ld-2.31.so 00007fa95aedd000 140K r-x-- ld-2.31.so 00007fa95af00000 32K r---- ld-2.31.so 00007fa95af09000 4K r---- ld-2.31.so 00007fa95af0a000 4K rw--- ld-2.31.so 00007fa95af0b000 4K rw--- [ anon ] 00007fff488fe000 132K rw--- [ stack ] 00007fff489f8000 12K r---- [ anon ] 00007fff489fb000 4K r-x-- [ anon ] ffffffffff600000 4K --x-- [ anon ] total 87168K
Z výpisu je patrné, že proces s 10000 korutinami v paměti zabírá přibližně 87MB, což zhruba odpovídá devíti kilobajtům na korutinu (což je zcela nepřesný výpočet, protože obsazená paměť sice poroste lineárně, ovšem nebude začínat v nule).
14. Spuštění 10000 souběžných úloh se sledováním vytížení CPU
V následujícím demonstračním příkladu spustíme 10000 souběžných úloh, ovšem tak, že doba trvání operace trio.sleep() bude snížena na minimum – jedinou sekundu. Budeme přitom zkoumat vytížení CPU:
import trio async def task(name, n, s): print(f"{name} task started") for i in range(n): print(f"{name} {i+1}/{n}") await trio.sleep(s) print(f"{name} task finished") async def main(): print("main started") async with trio.open_nursery() as nursery: for i in range(10000): nursery.start_soon(task, f"Task {i}", 10, 1) print("done") trio.run(main)
Takto spuštěný proces bude (obecně) v daný okamžik běžet na jediném jádru, které bude vytíženo téměř na sto procent, což je ostatně patrné i při pohledu na následující screenshot získaný nástrojem top:
Obrázek 1: Vytížení CPU na systému s osmi procesorovými jádry.
15. Komunikace mezi souběžnými úlohami s využitím kanálů
Knihovny či v některých případech dokonce i nové jazykové konstrukce umožňující používání kanálů (či front) pro asynchronní komunikaci mezi různými částmi vyvíjených aplikací, se v posledních několika letech těší poměrně velké popularitě. Ta je způsobena především dvěma faktory. První důvod spočívá ve snaze o zjednodušení návrhu (či porozumění) vyvíjené aplikace, zejména ve chvíli, kdy se v rámci jednoho programu předávají data (resp. objekty) mezi částmi, jejichž funkce může být dobře izolována od částí ostatních.
Druhý důvod je poněkud prozaičtější – v některých situacích je nutné dosáhnout zvýšení efektivity celé aplikace (například zvýšit počet odpovědí, které může server vygenerovat za určitou časovou jednotku) a přitom není možné či vhodné využívat řešení založené na použití většího množství vláken spravovaných přímo operačním systémem. Naprosto typickým příkladem jsou virtuální stroje JavaScriptu, které povětšinou umožňují běh aplikace v jediném vláknu (což je ovšem s ohledem na „kvalitu“ některých programových kódů spíše výhodou…).
Některé programovací jazyky, zejména pak v tomto paralelně běžícím seriálu popisovaný jazyk Go, obsahují prostředky sloužící pro zajištění asynchronní komunikace přímo v syntaxi (a samozřejmě též v sémantice) jazyka. Konkrétně v případě jazyka Go se jedná o takzvané gorutiny, které jsou doplněny o specializované operace sloužící pro zápis či čtení dat z kanálů (channels). Tyto specializované operace jsou v jazyce Go představovány operátorem <- (ten má dva významy v závislosti na tom, zda je před operátorem uveden identifikátor představující kanál či nikoli).
Knihovna Trio je založena na korutinách, které sice běží souběžně, ale nikoli nutně paralelně. Nicméně i mezi korutinami je mnohdy nutné předávat data. I zde se pro tento účel používají kanály neboli channels. Ty se na jedné straně podobají frontám (queue), na straně druhé však mají poněkud odlišnou sémantiku s pevným rozdělením na část určenou pro posílání data a na část určenou pro čtení dat.
16. Základní vlastnosti kanálů nabízených knihovnou Trio
Kanálem je v knihovně Trio myšlena interní datová struktura sloužící pro zápis dat jednou úlohou (či větším množstvím úloh) a čtením dat další úlohou (nebo úlohami). Z pohledu programátora je kanál reprezentován dvojicí objektů, přičemž jeden z těchto objektů slouží pro zápis dat a druhý pro jejich čtení. To je největší rozdíl oproti klasickým frontám, kde jeden objekt (opět z pohledu programátora) nabízí obě operace – zápis i čtení dat. Kanál má specifikovanou kapacitu, tedy počet zpráv, které v něm mohou být uloženy, možné je vytvořit i kanál s nulovou kapacitou, což znamená, že každý zápis dat je blokující a musí být následován jejich čtením (z druhé strany kanálu). Kanál je možné uzavřít metodou aclose, ovšem toto uzavření je provedeno automaticky v případě, že je kanál vytvořen v bloku async with. Následuje typický příklad konstrukce kanálu se získáním dvojice objektů – jeden je určený pro zápis dat, druhý pro čtení dat. Kapacita je nulová:
async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(0)
Konstrukce kanálu s omezenou kapacitou interního bufferu:
async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(100)
Konstrukce kanálu s neomezenou kapacitou interního bufferu:
async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(math.inf)
17. Ukázka klasické úlohy typu producent-konzument
Klasickou úlohu typu producent-konzument s jediným producentem a jediným konzumentem je možné v knihovně Trio zapsat zcela idiomatickým způsobem. Producent použije „vysílací“ část kanálu a producent část „přijímací“. Jak producent, tak i konzument jsou vytvořeny společně s kanálem v bloku async with, čímž je zajištěno automatické uzavření všech prostředků. Tento demonstrační příklad lze později rozšířit, například tak, aby se použil kanál s kapacitou, větší množství producentů a/nebo větší množství konzumentů:
import trio async def producer(send_channel): for i in range(1, 10): message = f"message {i}" print(f"Producer: {message}") await send_channel.send(message) async def consumer(receive_channel): async for value in receive_channel: print(f"Consumer: received{value!r}") await trio.sleep(1) async def main(): async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(0) nursery.start_soon(producer, send_channel) nursery.start_soon(consumer, receive_channel) trio.run(main)
18. Obsah druhé části článku
Knihovna Trio nabízí programátorům i některé další užitečné techniky. Jedná se například o možnost předčasného ukončení souběžně běžících úloh, specifikace maximálního času pro provedení úlohy (timeout), vytvoření souběžných úloh z jiných souběžných úloh a v neposlední řadě o podporu monitoringu celé aplikace. S těmito technikami se podrobněji seznámíme v samostatném článku.
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- 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 - ThreadPoolExecutor
https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor - ProcessPoolExecutor
https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor - asyncio — Asynchronous I/O
https://docs.python.org/3/library/asyncio.html - Threads vs Async: Has Asyncio Solved Concurrency?
https://www.youtube.com/watch?v=NZq31Sg8R9E - Python Asynchronous Programming – AsyncIO & Async/Await
https://www.youtube.com/watch?v=t5Bo1Je9EmE - AsyncIO & Asynchronous Programming in Python
https://www.youtube.com/watch?v=6RbJYN7SoRs - Coroutines and Tasks
https://docs.python.org/3/library/asyncio-task.html - Python async/await Tutorial
https://stackabuse.com/python-async-await-tutorial/ - Demystifying Python's Async and Await Keywords
https://www.youtube.com/watch?v=F19R_M4Nay4 - Curio
https://curio.readthedocs.io/en/latest/ - Trio: a friendly Python library for async concurrency and I/O
https://trio.readthedocs.io/en/stable/ - Curio – A Tutorial Introduction
https://curio.readthedocs.io/en/latest/tutorial.html - unsync
https://github.com/alex-sherman/unsync - David Beazley – Die Threads
https://www.youtube.com/watch?v=xOyJiN3yGfU - Miguel Grinberg Asynchronous Python for the Complete Beginner PyCon 2017
https://www.youtube.com/watch?v=iG6fr81×HKA - Build Your Own Async
https://www.youtube.com/watch?v=Y4Gt3Xjd7G8 - The Other Async (Threads + Async = ❤️)
https://www.youtube.com/watch?v=x1ndXuw7S0s - Fear and Awaiting in Async: A Savage Journey to the Heart of the Coroutine Dream
https://www.youtube.com/watch?v=E-1Y4kSsAFc - Keynote David Beazley – Topics of Interest (Python Asyncio)
https://www.youtube.com/watch?v=ZzfHjytDceU - David Beazley – Python Concurrency From the Ground Up: LIVE! – PyCon 2015
https://www.youtube.com/watch?v=MCs5OvhV9S4 - Python Async basics video (100 million HTTP requests)
https://www.youtube.com/watch?v=Mj-Pyg4gsPs - Nathaniel J. Smith – Trio: Async concurrency for mere mortals – PyCon 2018
https://www.youtube.com/watch?v=oLkfnc_UMcE - Timeouts and cancellation for humans
https://vorpus.org/blog/timeouts-and-cancellation-for-humans/ - What is the core difference between asyncio and trio?
https://stackoverflow.com/questions/49482969/what-is-the-core-difference-between-asyncio-and-trio - Some thoughts on asynchronous API design in a post-async/await world
https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#the-curious-effectiveness-of-curio - Companion post for my PyCon 2018 talk on async concurrency using Trio
https://vorpus.org/blog/companion-post-for-my-pycon-2018-talk-on-async-concurrency-using-trio/ - Control-C handling in Python and Trio
https://vorpus.org/blog/control-c-handling-in-python-and-trio/ - Context Managers and Python's with Statement
https://realpython.com/python-with-statement/ - Notes on structured concurrency, or: Go statement considered harmful
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ - Structured concurrency explained – Part 1: Introduction
https://www.thedevtavern.com/blog/posts/structured-concurrency-explained/ - Structured concurrency
https://en.wikipedia.org/wiki/Structured_concurrency - Structured Concurrency
https://250bpm.com/blog:71/ - Python and Trio, where producers are consumers, how to exit gracefully when the job is done?
https://stackoverflow.com/questions/65304775/python-and-trio-where-producers-are-consumers-how-to-exit-gracefully-when-the