Python Concurrency Tutorial

Toto je shrnutí mého tutoriálu z PyConu 2020. Původní video a zdrojový kód najdete zde: https://github.com/santiagobasulto/pycon-concurrency-tutorial-2020

https://www.youtube.com/watch?v=18B1pznaU1o

Jedná se o stručný návod/tutoriál, jak efektivně psát souběžné programy pomocí jazyka Python. Souběžnost v jazyce Python může být matoucí. Existuje více modulů (threading, _thread, multiprocessing, subprocess). Existuje také tolik nenáviděný GIL, ale pouze pro CPython (PyPy a Jython GIL nemají). Kolekce nejsou bezpečné pro vlákna, s výjimkou některých implementačních detailů u CPythonu.

Cílem tohoto tutoriálu je poskytnout přízemní doporučení, jak přistupovat k jednotlivým případům použití souběžného (nebo paralelního) kódu.

Všimněte si, že na rozdíl od některých jiných tradičnějších tutoriálů Pythonu „pouze pro multithreading“ začínám shrnutím architektury počítače i pojmů operačních systémů. Pochopení těchto základních pojmů je zásadní pro správné sestavení souběžných programů. Pokud jste již s těmito tématy obeznámeni, přeskočte prosím tyto části.

Tento výukový kurz je rozdělen do následujících částí:

  • Konkurence vs. paralelismus
  • Architektura počítače
  • Úloha operačního systému
  • Vlákna v Pythonu
  • Synchronizace vláken
  • Multiprocesing
  • Knihovny vysoké úrovně: concurrent.futures a parallel

Konkurence vs. paralelismus

Paralelismus je, když běží několik úloh současně. Je to konečný cíl souběžných programů. Souběžnost je méně než paralelismus, znamená to, že spouštíme několik úloh a žonglujeme s nimi ve stejném časovém úseku. V každém konkrétním okamžiku však děláme pouze jednu najednou.

Použijeme-li je na vaření, jednalo by se o příklady souběžnosti a paralelismu.

Konkurenci má pouze jedna osoba v kuchyni:

  • Začněte krájet cibuli
  • Začněte zahřívat pánev
  • Dokončete krájení cibule

V tomto případě je jasné, že nemůžeme dělat více věcí SOUČASNĚ. Můžeme je všechny začít, ale musíme se odrážet sem a tam, abychom je zvládli.

Paralelizmus má více lidí v kuchyni:

  • Osoba 1 krájí cibuli
  • Osoba 2 krájí červenou papriku
  • Osoba 3 čeká, až se rozpálí pánev

V tomto případě se dělá několik úkolů současně.

Architektura počítače

Všechny moderní počítače lze zjednodušeně označit pomocí von Neumannovy architektury, která má 3 hlavní části:

Architektura von Neumann

Váš kód bude vždy provádět úlohy související s každou z těchto komponent. Některé instrukce jsou „výpočetní“ ( x + 1), některé paměťové (x = 1) a některé I/O (fp = open('data.csv')).

Pro správné pochopení detailů souběhu v jazyce Python je důležité mít na paměti různé „přístupové doby“ těchto komponent. Přístup k procesoru je mnohem rychlejší než přístup k paměti RAM, o I/O ani nemluvě. Existuje studie srovnávající latenci počítače s relativními časy člověka: pokud cyklus procesoru představuje 1 sekundu lidského času, trvá síťový požadavek ze San Francisca do Hongkongu 11 let.

Latence v lidském měřítku (zdroj)

Radikální časový rozdíl u těchto „instrukcí“ bude zásadní pro diskusi o vláknech a GIL Pythonu.

Úloha operačního systému

Kdybych sestavoval univerzitní kurz o konkurenci, měl bych celou přednášku jen o historii operačních systémů. Jedině tak pochopíte, jak se mechanismy souběžnosti vyvíjely.

První operační systémy zaměřené na spotřebitele (myslím MS-DOS) nepodporovaly multitasking. V určitém okamžiku mohl běžet nanejvýš jen jeden program. Po nich následovaly systémy s „kooperativním multitaskingem“ (Windows 3.0). Tento typ multitaskingu připomínal spíše „smyčku událostí“ (představte si asyncio v Pythonu) než moderní vícevláknový program. V operačním systému s kooperativním multitaskingem je aplikace zodpovědná za „uvolnění“ procesoru, když už není potřeba, a nechá běžet ostatní aplikace. To samozřejmě nefungovalo, protože nemůžete věřit, že každá aplikace (nebo dokonce vaši vývojáři jsou dostatečně bystří na to, aby správně rozhodli, kdy uvolnit procesor). Moderní operační systémy (Windows 95, Linux atd.) přešly při návrhu multitaskingu na „preemptivní multitasking“, kdy o tom, které aplikace dostanou procesor, jak často a na jak dlouho, rozhoduje operační systém.

Operační systém je tedy tím, kdo rozhoduje o tom, které procesy budou spuštěny a na jak dlouho. Díky tomu má aplikace stále pocit, že je „multitaskingová“, i když má k dispozici pouze 1 procesor. Plánovač operačního systému velmi rychle přepíná procesy tam a zpět.

Na následujícím obrázku běží současně 3 procesy (P1, P2, P3), které sdílejí stejný procesor (modrá čára je čas procesoru). P1 běží poměrně dlouho, než se rychle přepne na P2 a pak na P3 atd.

3 procesy běží „současně“

Proces

Proces je abstrakce, kterou operační systém používá ke spuštění vašeho kódu. Zabalí váš kód do tohoto procesu a přiřadí mu paměť a další sdílené prostředky. Abstrakce procesu umožňuje operačnímu systému „rozlišovat“ mezi spuštěnými programy, takže mezi nimi vznikají konflikty. Například proces 1 nemůže přistupovat k paměti rezervované procesem 2. Je také důležitá z hlediska bezpečnosti uživatelů. Pokud uživatel 1 spustí proces, bude tento proces moci číst soubory přístupné tomuto uživateli.

Následující schéma obsahuje vizuální znázornění procesu. Proces obsahuje kód, který má vykonat, alokovanou paměť RAM a všechna data vytvořená programem (proměnné, otevřené soubory atd.).

Vizuální znázornění procesu OS

Nyní můžeme vidět stejný „program“ (stejný kus kódu) běžící ve více procesech současně.

Můžeme nyní přejít k druhé fázi: jak je vlastně souběžnosti (nebo dokonce paralelismu) dosaženo? Z pohledu vás jako vývojáře existují dva mechanismy, které můžete použít k dosažení „souběžnosti“: multithreading nebo multiprocessing.

Multithreading

Vlákna jsou souběžné toky provádění v rámci jednoho procesu. Proces spouští několik vláken provádění, která se „vyvíjejí“ nezávisle na tom, co mají dělat.

Řekněme, že máme program, který musí stáhnout data ze tří webových stránek a pak je spojit do závěrečné zprávy. Reakce každé webové stránky s daty trvá přibližně dvě sekundy. Jednovláknový program by vypadal nějak takto:

d1 = get_website_1() # 2 secs
d2 = get_website_2() # 2 secs
d3 = get_website_3() # 2 secscombine(d1, d2, d3) # very fast

Celková doba provádění bude >6 sekund:

Jednovláknový program

Vícevláknový program začne stahovat data ze tří webových stránek současně a počká, až budou všechny hotové, aby mohl vytvořit závěrečnou zprávu.

t1 = Thread(get_website_1)
t2 = Thread(get_website_2)
t3 = Thread(get_website_3)wait_for_threads()d1 = t1.result()
d2 = t2.result()
d3 = t3.result()combine(d1, d2, d3)

Vykonání se nyní bude blížit ~2 sekundám, protože tři webové stránky budou reagovat přibližně ve stejnou dobu:

Vícevláknový program

V Pythonu se vlákna vytvářejí pomocí třídy Thread, z modulu threading, a několik příkladů můžete vidět v prvním sešitě z repozitáře mého výukového programu: 1. Základy vláken.ipynb.

Vlákna a sdílená data

Vlákna jsou explicitně vytvořena vaším programem, takže žijí v rámci jednoho procesu. To znamená, že sdílejí všechna stejná data z procesu. Mohou číst a měnit místní proměnné a přistupovat ke stejné paměti nebo stejným prostředkům (soubory, síťové sokety atd.).

Vlákna sdílejí data procesu

To může způsobit některé problémy, speciálně neočekávané mutace sdílených dat a Race Conditions. Příklad závodní podmínky si můžete prohlédnout v sešitě 2. Data vláken & Podmínky závodu.ipynb. Ve stejném sešitě zavádíme některé mechanismy „synchronizace“ vláken, abychom se vyhnuli Race Conditions, ale tytéž mechanismy mohou potenciálně přinést problém Deadlock. V sešitě je příklad Deadlocku: 3. Deadlocks.ipynb.

Producenti, konzumenti a fronty

Problémy se synchronizací lze vyřešit pouhou změnou paradigmatu.

Nejlepším způsobem, jak se vyhnout problémům se synchronizací, je vyhnout se synchronizaci úplně.

Toho lze dosáhnout použitím vzoru Producer-Consumer. Vlákna Producer vytvářejí „čekající úkoly“, které je třeba provést, a poté je umístí do sdílené fronty. Vlákna konzumentů tyto úkoly z fronty přečtou a provedou vlastní práci.

V jazyce Python je třída Queue z modulu queue frontou bezpečnou pro vlákna, která se pro tyto účely používá.

V sešitě
4 je příklad modelu Producer-Consumer. Model Producer-Consumer.ipynb.

Globální zámek interpretu Pythonu

GIL je jednou z nejnenáviděnějších funkcí Pythonu. Ve skutečnosti není tak špatná, ale má hroznou pověst.

GIL je „globální“ zámek, který umisťuje interpret jazyka Python a který zaručuje, že v daném okamžiku může k procesoru přistupovat pouze 1 vlákno. To zní hloupě: proč se budete starat o vícevláknový kód, když k CPU bude mít v daném okamžiku stejně přístup jen jedno vlákno?“

Tady je důležité pochopit, že váš kód není jen o využití CPU. Váš kód bude provádět různé úlohy, jako je čtení souborů, získávání informací ze sítě atd. Vzpomínáte si na naše přístupové časy s latencí? Tady jsou znovu:

Latence v lidském měřítku (zdroj)

To je klíč k boji proti omezení GIL. V okamžiku, kdy vlákno potřebuje něco přečíst ze souboru, může uvolnit procesor a nechat běžet ostatní vlákna. Než pevný disk data vrátí, bude to trvat dlouho. je to proto, že většina programů je takzvaně I/O-Bound. To znamená, že používají hodně I/O funkcí (síť, souborový systém atd.).

V sešitě si můžete prohlédnout „důkaz“ úlohy vázané na vstupy a výstupy: 5. Python GIL.ipynb.

Multiprocesing

Některé další programy jsou nevyhnutelně CPU-Bound, například programy, které potřebují provádět mnoho výpočtů. Někdy je třeba vše urychlit. Pokud použijete multithreading pro úlohy CPU-Bound, zjistíte, že vaše programy nejsou díky GIL rychlejší (ve skutečnosti mohou být ještě pomalejší).

Vstupte do multiprocessingu, druhého mechanismu, který máme k dispozici pro psaní souběžných programů. Místo toho, abychom měli více vláken, budeme ve skutečnosti vytvářet více podřízených procesů, které se budou starat o souběžné provádění různých úloh.

V sešitě 6 je příklad programu založeného na multiprocesingu. Multiprocessing.ipynb

concurrent.futures

Na závěr svého výkladu bych rád upozornil, že existuje modul vyšší úrovně, který je součástí standardní knihovny Pythonu a který by se měl používat, pokud je to možné: concurrent.futures.

Poskytuje vysokoúrovňové abstrakce pro vytváření poolu vláken a procesů a je dostatečně vyspělý na to, aby byl považován, kdykoli je to možné, za výchozí alternativu pro psaní souběžného kódu.

Tady to máte! Dejte mi vědět, pokud máte nějaké dotazy. Rád na ně odpovím.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.