Obsah
1. Základní řešení problému typu producent-konzument v knihovně Trio
2. Specifikace maximální doby čekání na zprávu popř. pro poslání zprávy do blokovaného kanálu
3. Přeskočení blokující operace namísto vyhození výjimky
4. Explicitní uzavření kanálu v korutině, reakce na uzavření kanálu dalšími korutinami
5. Problematika uzavření kanálu používaného větším množstvím korutin
6. Vyřešení předchozího problému – „naklonování“ kanálu
7. Využití kontextových informací
11. Shrnutí – technologie pro souběžné a paralelní úlohy v Pythonu
12. Tři nejčastěji používané strategie
13. Jaké řešení je tedy nejvhodnější?
14. Repositář s demonstračními příklady
15. Předchozí články z miniseriálu o souběžných úlohách v Pythonu
1. Základní řešení problému typu producent-konzument v knihovně Trio
V první polovině dnešního článku dokončíme popis problematiky, které jsme se věnovali již minule. Připomeňme si, že jsme se zabývali způsobem komunikace mezi korutinami spravovanými knihovnou Trio s využitím jednosměrných komunikačních kanálů (channel), které mohou být použity buď jako jednoduché mailboxy, nebo mohou mít přiřazen buffer s určitou kapacitou (potom se do jisté míry chovají jako fronty – queue).
Mnoho reálných úloh je obdobou problematiky producent-konzument, což je architektura, v níž jsou od sebe odděleny zdroje dat (nebo úloh) a cíle dat (vykonavatelé úloh). Jak producenti, tak i konzumenti mohou být realizovány s využitím korutin, což je zvláště užitečné ve chvíli, kdy vykonávají mnoho I/O operací. V případě použití knihovny Trio může komunikace probíhat s využitím kanálů resp. v tom nejjednodušším případě s využitím jediného kanálu:
import trio num_producers = 5 num_consumers = 20 async def producer(id, send_channel): for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") await send_channel.send(message) async def consumer(id, receive_channel): async for value in receive_channel: print(f"Consumer #{id}: 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) for id in range(num_producers): nursery.start_soon(producer, id, send_channel) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel) trio.run(main)
Obecně platí, že se producenti budou o zaslané úlohy dělit:
Producer #4: message 1 Producer #3: message 1 Producer #2: message 1 Producer #1: message 1 Producer #0: message 1 Producer #0: message 2 Consumer #15: received'message 1' Producer #1: message 2 Consumer #16: received'message 1' Producer #2: message 2 Consumer #17: received'message 1' Producer #3: message 2 Consumer #18: received'message 1' Producer #4: message 2 Consumer #19: received'message 1' ... ... ... Consumer #16: received'message 8' Producer #2: message 9 Consumer #17: received'message 8' Producer #3: message 9 Consumer #18: received'message 8' Producer #4: message 9 Consumer #19: received'message 8' Consumer #6: received'message 9' Consumer #5: received'message 9' Consumer #0: received'message 9' Consumer #1: received'message 9' Consumer #2: received'message 9'
V dalších kapitolách si popíšeme některé problémy, které mohou nastat i způsoby jejich řešení.
2. Specifikace maximální doby čekání na zprávu, popř. pro poslání zprávy do blokovaného kanálu
Poslání zprávy do kanálu či naopak přečtení zprávy z kanálu je obecně blokující operace, která ve výchozím nastavení bude čekat potenciálně nekonečnou dobu na to, až bude kanál uvolněn pro poslání zprávy či naopak až se v něm objeví zpráva, na kterou čeká konzument. Ovšem, jak jsme si již řekli minule, je možné specifikovat maximální dobu čekání na dokončení nějaké operace či operací. V našem konkrétním případě to znamená, že se bude jednat o operaci poslání zprávy:
with trio.fail_after(producer_timeout): await send_channel.send(message)
Nebo naopak o operaci příjmu zprávy:
with trio.fail_after(consumer_timeout): value = await receive_channel.receive() print(f"Consumer #{id}: received{value!r}") await trio.sleep(1)
V obou uvedených případech platí, že pokud nedojde k dokončení operace v nastaveném čase je vyhozena výjimka, na kterou lze v dalším kódu adekvátně reagovat.
Podívejme se nyní na demonstrační příklad, kde je tento koncept použit:
import trio num_producers = 5 num_consumers = 20 consumer_timeout = 2 producer_timeout = 2 async def producer(id, send_channel): for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") with trio.fail_after(producer_timeout): await send_channel.send(message) async def consumer(id, receive_channel): while True: with trio.fail_after(consumer_timeout): value = await receive_channel.receive() print(f"Consumer #{id}: 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) for id in range(num_producers): nursery.start_soon(producer, id, send_channel) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel) trio.run(main)
Po spuštění skriptu začnou pracovat jak producenti, tak i konzumenti:
Producer #4: message 1 Producer #3: message 1 Producer #2: message 1 Producer #1: message 1 Producer #0: message 1 Consumer #19: received'message 1' Producer #4: message 2 Consumer #18: received'message 1' Producer #3: message 2 Consumer #17: received'message 1' Producer #2: message 2 Consumer #16: received'message 1' Producer #4: message 1 Producer #3: message 1 Producer #2: message 1 Producer #1: message 1 Producer #0: message 1 Consumer #19: received'message 1' Producer #4: message 2 Consumer #18: received'message 1' Producer #3: message 2 Consumer #17: received'message 1' Producer #2: message 2 Consumer #16: received'message 1' ... ... ...
Ovšem každý producent vytvoří pouze určitý počet zpráv a potom je ukončen. Následně je vyhozena výjimka typu TooSlowError následovaná ukončením všech operací:
Producer #3: message 5 Consumer #0: received'message 4' Producer #4: message 5 Traceback (most recent call last): File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_timeouts.py", line 106, in fail_at yield scope File "trio_26_timeouts.py", line 23, in consumer await trio.sleep(1) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_timeouts.py", line 76, in sleep await sleep_until(trio.current_time() + seconds) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_timeouts.py", line 57, in sleep_until await sleep_forever() File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_timeouts.py", line 40, in sleep_forever await trio.lowlevel.wait_task_rescheduled(lambda _: trio.lowlevel.Abort.SUCCEEDED) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_traps.py", line 166, in wait_task_rescheduled return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap() File "/home/ptisnovs/.local/lib/python3.8/site-packages/outcome/_impl.py", line 138, in unwrap raise captured_error File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 1173, in raise_cancel raise Cancelled._create() trio.Cancelled: Cancelled During handling of the above exception, another exception occurred: Traceback (most recent call last): File "trio_26_timeouts.py", line 35, 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_26_timeouts.py", line 32, in main nursery.start_soon(consumer, id, receive_channel) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 813, in __aexit__ raise combined_error_from_nursery File "trio_26_timeouts.py", line 23, in consumer await trio.sleep(1) File "/usr/lib/python3.8/contextlib.py", line 131, in __exit__ self.gen.throw(type, value, traceback) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_timeouts.py", line 108, in fail_at raise TooSlowError trio.TooSlowError
3. Přeskočení blokující operace namísto vyhození výjimky
V případě timeoutu se obecně očekávají dvě reakce – buď dojde k vyhození výjimky, nebo se bude operace posílání/příjmu do kanálu ignorovat. Prozatím umíme vyhodit výjimku, pokud je kanál blokovaný po delší dobu, než je doba specifikovaná ve funkci fail_after. Alternativou může být pouhé přeskočení dané operace (tedy „pokus se deset sekund čekat na zprávu a potom pokračuj dále, pokud není zpráva přijata“). Tento alternativní přístup je možné realizovat s využitím funkce move_on_after. Podívejme se tedy na způsob úpravy předchozího demonstračního příkladu tak, aby byl použit tento alternativní přístup:
import trio num_producers = 5 num_consumers = 20 consumer_timeout = 2 producer_timeout = 2 async def producer(id, send_channel): for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") with trio.move_on_after(producer_timeout): await send_channel.send(message) async def consumer(id, receive_channel): while True: print(f"Consumer #{id}: trying to receive message") with trio.move_on_after(consumer_timeout): value = await receive_channel.receive() print(f"Consumer #{id}: 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) for id in range(num_producers): nursery.start_soon(producer, id, send_channel) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel) trio.run(main)
Chování po spuštění:
Consumer #19: trying to receive message Consumer #18: trying to receive message Consumer #17: trying to receive message Consumer #16: trying to receive message Consumer #15: trying to receive message ... ... ...
Začátek práce producentů:
... ... ... Producer #4: message 1 Producer #3: message 1 Producer #2: message 1 Producer #1: message 1 Producer #0: message 1 Consumer #19: received'message 1' Producer #4: message 2 Consumer #18: received'message 1' Producer #3: message 2 Consumer #17: received'message 1' Producer #2: message 2 Consumer #16: received'message 1' ... ... ...
Po poslání všech zpráv:
... ... ... Consumer #14: received'message 9' Consumer #5: received'message 9' Consumer #6: received'message 9' Consumer #7: received'message 9' Consumer #13: trying to receive message Consumer #14: trying to receive message Consumer #5: trying to receive message Consumer #6: trying to receive message Consumer #7: trying to receive message Consumer #12: trying to receive message Consumer #11: trying to receive message Consumer #10: trying to receive message Consumer #15: trying to receive message Consumer #16: trying to receive message Consumer #17: trying to receive message Consumer #18: trying to receive message Consumer #19: trying to receive message ... ... ...
4. Explicitní uzavření kanálu v korutině, reakce na uzavření kanálu dalšími korutinami
Prozatím jsme na straně producenta používali přibližně tento programový kód (někdy doplněný o řešení timeoutů):
async def producer(id, send_channel): for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") await send_channel.send(message)
V takových případech producent pouze ukončil posílání zpráv, ovšem nijak neinformoval další korutiny, že již další zprávy nebude posílat. Samozřejmě by bylo možné navrhnout si nějaký komunikační protokol, ovšem existuje i jednodušší řešení – prostě komunikační kanál uzavřít. Stav kanálu (uzavřen/otevřen) je další informací, kterou mezi sebou korutiny mohou komunikovat, protože stav kanálu lze snadno zjistit. Automatické uzavření kanálu lze realizovat takto:
async with send_channel: ... ... ... # po opuštění bloku with je kanál uzavřen
Na což může reagovat konzument ukončením smyčky async for:
async for value in receive_channel: ... ... ... # pokud se dostaneme sem, byl kanál uzavřen
Podívejme se nyní na ucelený demonstrační příklad používající tento potenciálně velmi užitečný koncept:
import trio async def producer(send_channel): async with 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) print("No more messages can be received!") 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)
Nyní se chování příkladu poměrně zásadním způsobem změní, protože konzument dokáže správně zareagovat na situaci, kdy je kanál uzavřen – namísto čekání na další zprávu je přijímací programová smyčka jednoduše a bez dalšího čekání ukončena:
Producer: message 1 Producer: message 2 Consumer: received'message 1' Consumer: received'message 2' Producer: message 3 Producer: message 4 Consumer: received'message 3' Consumer: received'message 4' Producer: message 5 Consumer: received'message 5' Producer: message 6 Producer: message 7 Consumer: received'message 6' Consumer: received'message 7' Producer: message 8 Producer: message 9 Consumer: received'message 8' Consumer: received'message 9' No more messages can be received!
5. Problematika uzavření kanálu používaného větším množstvím korutin
Předchozí příklad sice pracoval zcela korektně, ale bylo tomu pouze z toho důvodu, že byl použit pouze jediný producent zpráv. To vlastně znamená, že kanál byl uzavřen pouze jedenkrát. Ovšem ve chvíli, kdy by bylo použito větší množství producentů sdílejících stejný kanál, povede prakticky totožný programový kód k tomu, že se jednotliví producenti budou snažit kanál uzavřít popř. k tomu, že nějaký producent zapíše zprávu do již uzavřeného kanálu. Ostatně můžeme si to sami otestovat spuštěním následujícího skriptu:
import trio num_producers = 5 num_consumers = 20 async def producer(id, send_channel): async with send_channel: for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") await send_channel.send(message) async def consumer(id, receive_channel): async for value in receive_channel: print(f"Consumer #{id}: received{value!r}") await trio.sleep(id) print("No more messages can be received!") async def main(): async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(0) for id in range(num_producers): nursery.start_soon(producer, id, send_channel) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel) trio.run(main)
Skript (zcela podle očekávání) zpočátku pracuje korektně, ovšem ihned poté, co první (libovolný) producent kanál uzavře, dojde k vyhození výjimky při přístupu k již uzavřenému kanálu:
Producer #0: message 1 Producer #1: message 1 Producer #2: message 1 Producer #3: message 1 Producer #4: message 1 Consumer #4: received'message 1' Producer #4: message 2 Consumer #3: received'message 1' Producer #3: message 2 Consumer #2: received'message 1' Producer #2: message 2 Consumer #1: received'message 1' Producer #1: message 2 Consumer #0: received'message 1' Producer #0: message 2 Consumer #5: received'message 2' Producer #4: message 3 Consumer #6: received'message 2' Producer #3: message 3 Consumer #7: received'message 2' Producer #2: message 3 Consumer #8: received'message 2' Producer #1: message 3 Consumer #9: received'message 2' Producer #0: message 3 Consumer #10: received'message 3' Producer #4: message 4 Consumer #11: received'message 3' Producer #3: message 4 Consumer #12: received'message 3' Producer #2: message 4 Consumer #13: received'message 3' Producer #1: message 4 Consumer #14: received'message 3' Producer #0: message 4 Consumer #15: received'message 4' Consumer #0: received'message 8' Producer #0: message 9 Consumer #0: received'message 9' No more messages can be received! Traceback (most recent call last): File "trio_28_multiple_channel_close.py", line 32, 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_28_multiple_channel_close.py", line 29, in main nursery.start_soon(consumer, id, receive_channel) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_run.py", line 813, in __aexit__ raise combined_error_from_nursery File "trio_28_multiple_channel_close.py", line 13, in producer await send_channel.send(message) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_channel.py", line 178, in send await trio.lowlevel.wait_task_rescheduled(abort_fn) File "/home/ptisnovs/.local/lib/python3.8/site-packages/trio/_core/_traps.py", line 166, in wait_task_rescheduled return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap() File "/home/ptisnovs/.local/lib/python3.8/site-packages/outcome/_impl.py", line 138, in unwrap raise captured_error trio.ClosedResourceError
6. Vyřešení předchozího problému – „naklonování“ kanálu
Uzavírání kanálů producentem či konzumentem, aby se touto operací oznámilo ostatním komunikujícím stranám, že daná korutina už nebude posílat či přijímat zprávy, je poměrně elegantní a navíc i idiomatické řešení celého problému. Ovšem na druhou stranu pracuje korektně jen ve chvíli, kdy kanál uzavírá jediný producent či konzument, což by mohlo vést ke zbytečné komplikaci kódu (logika pro určení, kdo může a kdo již ne kanál zavřít). Knihovna Trio však programátorům nabízí řešení tohoto problému, a to „naklonováním“ kanálu. Z jednoho kanálu tak lze vytvořit klony, které ovšem sdílí data a ve chvíli, kdy je uzavřen poslední naklonovaný kanál, se uzavře i skutečný kanál. Toto řešení tedy přenáší celý problém na stranu Tria a bude vypadat následovně:
async with send_channel, receive_channel: for id in range(num_producers): nursery.start_soon(producer, id, send_channel.clone()) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel.clone())
Žádné další úpravy není zapotřebí provádět:
import trio num_producers = 5 num_consumers = 20 async def producer(id, send_channel): async with send_channel: for i in range(1, 10): message = f"message {i}" print(f"Producer #{id}: {message}") await send_channel.send(message) async def consumer(id, receive_channel): async for value in receive_channel: print(f"Consumer #{id}: received{value!r}") await trio.sleep(id) print("No more messages can be received!") async def main(): async with trio.open_nursery() as nursery: send_channel, receive_channel = trio.open_memory_channel(0) async with send_channel, receive_channel: for id in range(num_producers): nursery.start_soon(producer, id, send_channel.clone()) for id in range(num_consumers): nursery.start_soon(consumer, id, receive_channel.clone()) trio.run(main)
Podívejme se na chování kódu po jeho spuštění. Začátek práce korutin:
Producer #4: message 1 Producer #3: message 1 Producer #2: message 1 Producer #1: message 1 Producer #0: message 1 Consumer #19: received'message 1' Producer #4: message 2 Consumer #18: received'message 1' Producer #3: message 2 Consumer #17: received'message 1' Producer #2: message 2 Consumer #16: received'message 1' Producer #1: message 2 Consumer #15: received'message 1'
Ukončení práce producentů zpráv:
Producer #4: message 9 Consumer #0: received'message 8' Consumer #0: received'message 9' Consumer #0: received'message 9' Consumer #0: received'message 9' Consumer #0: received'message 9' Consumer #0: received'message 9' No more messages can be received! No more messages can be received! No more messages can be received! No more messages can be received! No more messages can be received! No more messages can be received!
Nyní začínají konzumenti hlásit, že nedostanou další data a sami sebe korektně ukončí:
No more messages can be received! No more messages can be received! No more messages can be received! No more messages can be received! No more messages can be received!
7. Využití kontextových informací
V Pythonu nalezneme knihovnu nazvanou contextvars. Tato knihovna slouží pro vytváření a přístup ke kontextovým proměnným (či informacím), a to z mnoha míst kódu. Jedná se o vylepšenou alternativu ke globálním proměnným, s nimiž se v programech s větším množstvím korutin pracuje dost nešikovně (a obecně se jedná spíše o problematickou vlastnost jazyka). Naproti tomu je mnohdy užitečné mít možnost sdílet informace mezi korutinami bez toho, aby se do všech funkcí předávaly ty stejné parametry (například informace pro logování atd.). A právě v těchto případech je možné použít kontextové proměnné.
Balíček s podporou kontextových proměnných se importuje takto:
import contextvars
Kontextovou proměnnou lze kdykoli vytvořit zápisem:
additional_info = contextvars.ContextVar("additional_info")
Tato proměnná podporuje metody .get a .set, které lze volat z libovolné korutiny, bez nutnosti další synchronizace.
8. Zámky
V knihovně Trio nalezneme i realizaci klasických zámků (lock) neboli mutexů. Zámkem je v kontextu souběžných úloh myšleno synchronizační primitivum, které (pochopitelně při správném použití) zajišťuje výhradní přístup k nějakým sdíleným prostředkům, například k bloku paměti (sdílená proměnná či objekt), otevřenému socketu atd. Zámky obecně podporují dvě operace – acquire pro získání zámku a release pro jeho uvolnění. Přitom platí, že zámek může vlastnit pouze jediná korutina. V případě, že se dvě či více korutin pokusí o získání zámku, „vyhraje“ jedna z těchto korutin a další korutiny čekají na jeho uvolnění. Díky použití zámků lze tedy „serializovat“ přístup k nějakému prostředku. Hrozí však nebezpečí, že pokud budou korutiny pracovat s větším množstvím zámků, dojde k deadlocku.
Dostupné metody třídy trio.Lock:
- acquire
- acquire_nowait
- locked
- release
- statistics
async with objekt_typu_zámek: ... ... ...
Do tohoto bloku se vstoupí jen ve chvíli, kdy korutina získá zámek. A po opuštění bloku dojde automaticky k uvolnění zámku, a to i v případě vyhození výjimky atd.
9. Příklad použití zámku
Podívejme se nyní na to, jakým způsobem lze se zámky pracovat v případě, že je vytvořeno několik korutin spouštějících totožný programový kód. Povšimněte si, že zámek je získán a současně i posléze uvolněn v bloku async with. To mj. znamená, že se na uvolnění zámku nezapomene. Navíc se jedná o idiomatický přístup, který známe i z dalších oblastí Tria:
import trio num_workers = 10 async def worker(id, lock): while True: # pokus o ziskani zamku s jeho naslednym automatickym uvolnenim async with lock: print(f"Worker #{id}: acquires lock") await trio.sleep(1) # zde je jiz zamek uvolnen async def main(): async with trio.open_nursery() as nursery: # konstrukce zamku lock = trio.Lock() for id in range(num_workers): nursery.start_soon(worker, id, lock) trio.run(main)
10. Férové získávání zámků
Zajímavé bude vysledovat, jakým způsobem je zámek po uvolnění předáván další korutině. Obecně totiž není specifikováno, které korutině bude zámek předán (a v mnoha programovacích jazycích resp. knihovnách tak může zámek stále získávat stále ta samá korutina a stále se bude jednat o korektní chování), ovšem v knihovně Trio (minimálně v současné verzi) je realizován algoritmus zajišťující určitou míru férovosti – viz též Should our locks (and semaphores, queues, etc.) be fair?. Ostatně spuštěním výše uvedeného demonstračního příkladu se můžeme sami přesvědčit, do jaké míry je předávání zámku férové či nikoli:
Worker #0: acquires lock Worker #1: acquires lock Worker #2: acquires lock Worker #3: acquires lock Worker #4: acquires lock Worker #5: acquires lock Worker #6: acquires lock Worker #7: acquires lock Worker #8: acquires lock Worker #9: acquires lock Worker #0: acquires lock Worker #1: acquires lock Worker #2: acquires lock Worker #3: acquires lock Worker #4: acquires lock Worker #5: acquires lock ... ... ...
11. Shrnutí – technologie pro souběžné a paralelní úlohy v Pythonu
V dnes končícím miniseriálu o souběžných, popř. v některých případech i paralelních úlohách realizovaných v programovacím jazyku Python jsme se seznámili s několika zajímavými a taktéž užitečnými technologiemi. Připomeňme si, že se jednalo jak o balíčky (knihovny) určené pro jazyk Python, tak i o rozšíření samotného Pythonu o nové konstrukce realizované klíčovými slovy async a await, které jsou typicky zkombinovány s již existujícími klíčovými slovy def, with a for.
12. Tři nejčastěji používané strategie
Všechna již popsaná technologická řešení můžeme zobecnit a následně rozdělit do tří kategorií podle toho, jak jsou vlastně souběžné a popř. i plně paralelně běžící úlohy realizovány:
- První způsob spočívá v tvorbě a spouštění souběžných a současně i potenciálně paralelně běžících úloh, přičemž každá úloha je realizována samostatným procesem (process) viditelným a řízeným přímo operačním systémem. Jednotlivé úlohy jsou sice vzájemně izolovány, ovšem mohou spolu komunikovat s využitím objektu typu Pipe. Konkrétně je tento problém řešen standardními knihovnami nazvanými multiprocessing a ProcessPoolExecutor (lze používat odděleně). Oddělení (isolation) je v porovnání s ostatními dvěma technologiemi nejlepší, ovšem nevýhodou jsou obecně větší nároky na systémové prostředky (několik běžících virtuálních strojů Pythonu).
- Druhý způsob není založen na samostatně běžících procesech, ale na vláknech (thread), které jsou vytvářeny a spouštěny v rámci jediného procesu (virtuálního stroje Pythonu). Vzájemná izolace jednotlivých úloh je v tomto případě menší a záleží vlastně jen na vývojáři, zda a jak zajistí přístup většího množství vláken do sdílených proměnných. Standardně se pro komunikaci mezi vlákny používají různé realizace front popř. zásobníků (Queue, LifoQueue, PriorityQueue a SimpleQueue). Práce s větším množstvím vláken je nabízena ve standardních knihovnách threading a taktéž ThreadPoolExecutor (opět lze používat odděleně).
- Třetí způsob nepoužívá ani samostatně běžící procesy ani (již méně samostatná) vlákna, ale vystačí si s využitím korutin (koroutines), přičemž v daný okamžik může souběžně běžet větší množství korutin, přičemž jedna korutina je aktivní. Operace s korutinami jsou realizovány přes již zmíněná klíčová slova async a await využívaná například standardní knihovnou asyncio s vlastní realizací asynchronní fronty a podporované dalšími knihovnami typu aiohttp. Kromě toho je práce s korutinami podporována i dalšími knihovnami, z nichž za zmínku stojí především knihovna Curio či ještě lépe navržená knihovna Trio, která z Curia částečně vychází.
13. Jaké řešení je tedy nejvhodnější?
Pravděpodobně není možné bez podrobnějších znalostí řešeného problému říci, která z výše zmíněných technologií je obecně nejlepší. Do značné míry totiž záleží na konkrétních požadavcích, které má vytvářená aplikace či služba splňovat. Pokud je například nutné řešit především mnoho souběžných I/O operací typu přístup k souborům, databázím, proudům (streams), komunikace přes HTTP atd. (což patří mezi typické úlohy programované v Pythonu), může být výhodné použít korutiny a například knihovnu Trio nebo dnes přece jen více standardní dvojici knihoven asyncio+aiohttp. Předností tohoto řešení je fakt, že v případě využití Tria má programátor široké možnosti řízení korutin, dokáže korektně reagovat na případné výjimky, které mohou v korutinách vzniknout atd. Na druhé straně toto řešení nezaručuje skutečně paralelní běh výpočtů atd.
Pokud je na druhou stranu nutné realizovat spíše výpočetně náročné úlohy (které lze do jisté míry paralelizovat), může se pro tento účel více hodit multiprocesing a (v menší míře) multithreading, který je ovšem v CPythonu (dnes asi nejpoužívanější implementace Pythonu) omezen kvůli existenci GILu (a aby nedošlo k mýlce – GIL v žádném případě neřeší korektní přístup ke sdíleným proměnným). Zcela subjektivně bych řekl, že multithreading je ze všech tří nabízených řešení pravděpodobně nejhůře reálně použitelnou a v mnoha ohledech i nebezpečnou technologií (vlákna nejsou na jedné straně oddělena, na straně druhé se špatně řídí a kontroluje jejich stav).
14. 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:
15. Předchozí články z miniseriálu o souběžných úlohách v Pythonu
- 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/
16. 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 - Lock (computer science)
https://en.wikipedia.org/wiki/Lock_(computer_science) - Zámek (informatika)
https://cs.wikipedia.org/wiki/Z%C3%A1mek_(informatika)