Vícevláknové v Python s Příkladem: Naučte se GIL v Python
⚡ Chytré shrnutí
Vícevláknové v Python Spouští několik vláken v jednom procesu, takže sdílejí paměť a pracují souběžně. Modul pro tvorbu vláken tato vlákna vytváří a spravuje, zatímco globální zámek interpretu omezuje skutečný paralelismus, takže tato technika je nejvhodnější pro úlohy vázané na vstup/výstup.

Jedno Python Programovací jazyk umožňuje používat multiprocessing nebo multithreading. V tomto tutoriálu se naučíte, jak psát vícevláknové aplikace v Python.
Co je vlákno?
Vlákno je jednotka provádění v souběžném programování. Multithreading je technika, která umožňuje CPU provádět mnoho úloh jednoho procesu současně. Tato vlákna se mohou provádět samostatně a zároveň sdílet své procesní zdroje.
Co je to proces?
Proces je v podstatě spuštěný program. Když v počítači spustíte aplikaci (například prohlížeč nebo textový editor), operační systém vytvoří proces.
V čem je multithreading Python?
Vícevláknové v Python Programování je dobře známá technika, při které více vláken v procesu sdílí svůj datový prostor s hlavním vláknem, což usnadňuje a zefektivňuje sdílení informací a komunikaci v rámci vláken. Vlákna jsou lehčí než procesy. Více vláken se může spouštět samostatně a zároveň sdílet procesní prostředky. Účelem multithreadingu je spouštět více úloh a funkcí současně.
Co je to Multiprocessing?
Multiprocesing umožňuje spouštět více nesouvisejících procesů současně. Tyto procesy nesdílejí své zdroje a nekomunikují prostřednictvím IPC.
Python Multithreading vs Multiprocessing
Pro pochopení procesů a vláknů zvažte tento scénář: Soubor .exe ve vašem počítači je program. Když jej otevřete, operační systém jej načte do paměti a procesor jej spustí. Instance programu, která je nyní spuštěna, se nazývá proces.
Každý proces má dvě základní složky:
- Jedno Code
- Data
Nyní může proces obsahovat jednu nebo více podčástí nazývaných vlákna. To závisí na architektuře operačního systému. Vlákno si můžete představit jako část procesu, kterou může operační systém spustit samostatně.
Jinými slovy, jedná se o proud instrukcí, které může operační systém spouštět nezávisle. Vlákna v rámci jednoho procesu sdílejí data tohoto procesu a jsou navržena tak, aby spolupracovala a usnadňovala paralelismus.
Proč používat multithreading?
Multithreading vám umožňuje rozdělit aplikaci na více dílčích úloh a spouštět tyto úlohy současně. Pokud používáte multithreading správně, můžete zlepšit rychlost vaší aplikace, výkon a vykreslování.
Python Vícevláknové zpracování
Python podporuje konstrukce pro multiprocessing i multithreading. V tomto tutoriálu se zaměříte především na implementaci vícevláknové aplikace s PythonExistují dva hlavní moduly, které lze použít ke zpracování vláken v Python:
- Jedno závit modulu a
- Jedno navlékání modul
Nicméně v Pythonexistuje také něco, čemu se říká globální zámek interpretu (GIL). Neumožňuje velké zvýšení výkonu a může dokonce snížit výkon některých vícevláknových aplikací. To vše se dozvíte v nadcházejících částech tohoto tutoriálu.
Moduly Thread a Threading
Dva moduly, o kterých se dozvíte v tomto tutoriálu, jsou závitový modul a závitový modul.
Modul vlákna je však již dávno zastaralý. Počínaje Python 3, byl označen jako zastaralý a je přístupný pouze jako _vlákno pro zpětnou kompatibilitu.
Měli byste použít vyšší úroveň navlékání modul pro aplikace, které chcete nasadit. Modul thread byl zde popsán pouze pro vzdělávací účely.
Závitový modul
Syntaxe pro vytvoření nového vlákna pomocí tohoto modulu je následující:
thread.start_new_thread(function_name, arguments)
Dobře, nyní jste probrali základní teorii, abyste mohli začít kódovat. Takže otevřete svůj IDLE nebo poznámkový blok a zadejte následující:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Uložte soubor a stiskněte F5 pro spuštění programu. Pokud bylo vše provedeno správně, toto je výstup, který byste měli vidět:
V následujících částech se dozvíte více o závodních podmínkách a o tom, jak je zvládat.
VYSVĚTLENÍ KÓDU
- Tyto příkazy importují moduly time a thread, které se používají ke zpracování provádění a zpoždění. Python vlákna.
- Zde jste definovali funkci nazvanou test_vlákna, které bude volat start_new_thread metoda. Funkce spustí smyčku while po dobu čtyř iterací a vypíše název vlákna, které ji volalo. Jakmile je iterace dokončena, vypíše zprávu oznamující, že vlákno dokončilo provádění.
- Toto je hlavní část vašeho programu. Zde jednoduše zavoláte start_new_thread metoda s test_vlákna funkci jako argument. Tím se vytvoří nové vlákno pro funkci, kterou předáte jako argument, a začne se její provádění. Upozorňujeme, že toto (thread_test) můžete nahradit jakoukoli jinou funkcí, kterou chcete spustit jako vlákno.
Modul závitování
Tento modul je implementací threadingu na vysoké úrovni v Python a de facto standard pro správu vícevláknových aplikací. Ve srovnání s modulem thread nabízí širokou škálu funkcí.
Struktura modulu Threading
Zde je seznam některých užitečných funkcí definovaných v tomto modulu:
| Název funkce | Description |
|---|---|
| activeCount() | Vrátí počet Vlákno předměty, které jsou stále živé. |
| currentThread() | Vrátí aktuální objekt třídy Thread. |
| vyjmenovat() | Vypisuje všechny aktivní objekty Thread. |
| isDaemon() | Vrátí hodnotu true, pokud je vlákno démon. |
| je naživu() | Vrací hodnotu true, pokud je vlákno stále živé. |
| Metody tříd vláken | |
| Start() | Spustí aktivitu vlákna. Musí být volána pouze jednou pro každé vlákno, protože při vícenásobném volání vyvolá chybu běhu. |
| běh() | Tato metoda označuje aktivitu vlákna a může být přepsána třídou, která rozšiřuje třídu Thread. |
| připojit se() | Blokuje provádění jiného kódu, dokud nebude ukončeno vlákno, na kterém byla zavolána metoda join(). |
Příběh: The Thread Class
Než začnete programovat vícevláknové programy pomocí modulu threading, je nezbytné porozumět třídě Thread. Třída thread je primární třída, která definuje šablonu a operace vlákna v... Python.
Nejběžnější způsob, jak vytvořit vícevláknový Python aplikace je deklarovat třídu, která rozšiřuje třídu Thread a přepisuje její metodu run().
Třída Thread v souhrnu znamená sekvenci kódu, která běží samostatně závit ovládání.
Takže při psaní vícevláknové aplikace budete dělat následující:
- definovat třídu, která rozšiřuje třídu Thread
- Přepsat __init__ konstruktérem
- Přepsat běh() metoda
Jakmile byl vytvořen objekt vlákna, Start() metoda může být použita k zahájení provádění této aktivity a připojit se() metodu lze použít k blokování všech ostatních kódů, dokud aktuální aktivita neskončí.
Nyní zkusme použít modul threading k implementaci vašeho předchozího příkladu. Znovu spusťte váš IDLE a zadejte následující:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
Toto bude výstup, když spustíte výše uvedený kód:
VYSVĚTLENÍ KÓDU
- Tato část je stejná jako náš předchozí příklad. Zde importujete moduly time a thread, které se používají ke zpracování provádění a zpoždění. Python vlákna.
- V tomto bitu vytváříte třídu s názvem threadtester, která dědí nebo rozšiřuje třídu Vlákno třída modulu pro vlákna. Toto je jeden z nejběžnějších způsobů vytváření vláken v PythonMěli byste však přepsat pouze konstruktor a běh() metodu ve vaší aplikaci. Jak můžete vidět ve výše uvedené ukázce kódu, __init__ metoda (konstruktor) byla přepsána. Podobně jste také přepsali běh() metoda. Obsahuje kód, který chcete spustit uvnitř vlákna. V tomto příkladu jste zavolali funkci thread_test().
- Toto je metoda thread_test(), která nabývá hodnoty i jako argument, v každé iteraci jej snižuje o 1 a prochází zbytek kódu, dokud se i nestane 0. V každé iteraci vypíše název aktuálně spuštěného vlákna a uspí se na několik sekund čekání (což je také bráno jako argument).
- thread1 = threadtester(1, “První vlákno”, 1) Zde vytváříme vlákno a předáváme tři parametry, které jsme deklarovali v __init__. Prvním parametrem je id vlákna, druhým parametrem je název vlákna a třetím parametrem je čítač, který určuje, kolikrát se má cyklus while spustit.
- thread2.start() Metoda start se používá ke spuštění provádění vlákna. Funkce start() interně volá metodu run() vaší třídy.
- thread3.join() Metoda join() blokuje provádění dalšího kódu a čeká, až skončí vlákno, na kterém byla volána.
Jak již víte, vlákna, která jsou ve stejném procesu, mají přístup k paměti a datům tohoto procesu. V důsledku toho, pokud se více než jedno vlákno pokusí o změnu nebo přístup k datům současně, mohou se objevit chyby.
V další části si ukážeme různé druhy komplikací, které se mohou objevit, když vlákna přistupují k datům a kritické sekci bez kontroly existujících přístupových transakcí.
Zablokování a závodní podmínky
Než se budeme zabývat deadlocky a závodními podmínkami, bude užitečné porozumět několika základním definicím týkajícím se souběžného programování:
- Kritická sekce: Jedná se o fragment kódu, který přistupuje k sdíleným proměnným nebo je upravuje a musí být proveden jako atomická transakce.
- Přepínání kontextu: Je to proces, kterým CPU ukládá stav vlákna před změnou z jedné úlohy na druhou, aby jej bylo možné později obnovit od stejného bodu.
Zablokování
Zablokování jsou nejobávanějším problémem, se kterým se vývojáři potýkají při psaní souběžných/vícevláknových aplikací v PythonNejlepší způsob, jak pochopit deadlocky, je použít klasický příklad problému z informatiky, známý jako Stravování PhiloSophers Problém.
Problém pro filozofy stolování je následující:
Pět filozofů sedí u kulatého stolu s pěti talíři špaget (druh těstovin) a pěti vidličkami, jak je znázorněno na obrázku.
Stravování PhiloSophers Problém
V každém okamžiku musí filozof buď jíst, nebo přemýšlet.
Navíc, filozof musí vzít dvě vidličky vedle něj (tj. levou a pravou vidličku), než může sníst špagety. Problém uváznutí nastává, když všech pět filozofů zvedne své správné vidličky současně.
Protože každý z filozofů má jednu vidličku, budou všichni čekat, až ostatní vidličku odloží. V důsledku toho nikdo z nich nebude moci jíst špagety.
Podobně v souběžném systému dochází k uváznutí, když se různá vlákna nebo procesy (filosofové) snaží získat sdílené systémové prostředky (forky) ve stejnou dobu. Výsledkem je, že žádný z procesů nedostane šanci se spustit, protože čekají na další prostředek držený nějakým jiným procesem.
Závodní podmínky
Soubojový stav je nežádoucí stav programu, ke kterému dochází, když systém provádí dvě nebo více operací současně. Uvažujme například tento jednoduchý cyklus for:
i=0; # a global variable for x in range(100): print(i) i+=1;
Pokud vytvoříte n U všech vláken, která tento kód spouštějí najednou, nelze po dokončení provádění programu určit hodnotu i (kterou sdílejí všechna vlákna). Je to proto, že v reálném prostředí s více vlákny se vlákna mohou překrývat a hodnota i, kterou jedno vlákno načetlo a upravilo, se může mezitím změnit, když k ní přistupuje jiné vlákno.
Toto jsou dvě hlavní třídy problémů, které se mohou vyskytnout ve vícevláknovém nebo distribuovaném prostředí. Python aplikace. V další části se dozvíte, jak tento problém překonat synchronizací vláken.
Synchronizující vlákna
Aby bylo možné řešit spory, zablokování a další problémy založené na vláknech, modul podprocesů poskytuje Zámek objekt. Myšlenka je taková, že když vlákno chce přístup ke konkrétnímu zdroji, získá zámek pro tento zdroj. Jakmile vlákno uzamkne určitý prostředek, žádné jiné vlákno k němu nebude mít přístup, dokud nebude zámek uvolněn. V důsledku toho budou změny zdroje atomické a rasové podmínky budou odvráceny.
Zámek je nízkoúrovňové synchronizační primitivum implementované serverem _vlákno modul. Zámek se může v daném okamžiku nacházet v jednom ze dvou stavů: zamčený or odemčený. Podporuje dvě metody:
- získat(): Pokud je stav uzamčení odemčený, volání metody acquire() změní stav na uzamčený a vrátí funkci. Pokud je však stav uzamčen, volání metody acquire() je blokováno, dokud metoda release() není volána jiným vláknem.
- uvolnění(): Metoda release() se používá k nastavení stavu na odemčeno, tj. k uvolnění zámku. Může být voláno jakýmkoli vláknem, ne nutně tím, které získalo zámek.
Zde je příklad použití zámků ve vašich aplikacích. Spusťte IDLE a zadejte následující:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Nyní stiskněte F5. Měli byste vidět výstup takto:
VYSVĚTLENÍ KÓDU
- Zde jednoduše vytvoříte nový zámek zavoláním na závitování.Lock() tovární funkce. Interně Lock() vrací instanci nejúčinnější konkrétní třídy Lock, která je spravována platformou.
- V prvním příkazu získáte zámek voláním metody purchase() . Po udělení zámku můžete tisknout "získán zámek" do konzole. Jakmile je dokončen veškerý kód, který má vlákno spouštět, uvolníte zámek voláním metody release().
Teorie je sice dobrá, ale jak víte, že zámek skutečně fungoval? Pokud se podíváte na výstup, uvidíte, že každý z příkazů print tiskne přesně jeden řádek po druhém. Připomeňme si, že v dřívějším příkladu byly výstupy funkce print nahodilé, protože k metodě print() přistupovalo více vláken současně. Zde je funkce print volána až po získání zámku. Výstupy se tedy zobrazují jeden po druhém a řádek po řádku.
Kromě zámků, Python také podporuje některé další mechanismy pro zpracování synchronizace vláken, jak je uvedeno níže:
- RLocks
- Semaphores
- Podmínky
- Události a
- Bariéry
Global Interpreter Lock (a jak se s tím vypořádat)
Než se pustíme do detailů PythonV rámci GIL si definujme několik pojmů, které budou užitečné pro pochopení následující části:
- Kód vázaný na CPU: označuje jakýkoli kód, který bude přímo spuštěn CPU.
- Kód vázaný na I/O: může se jednat o jakýkoli kód, který přistupuje k souborovému systému prostřednictvím operačního systému.
- CPython: je to reference uskutečnění of Python a lze jej popsat jako interpret napsaný v C and Python (programovací jazyk).
V čem je GIL Python?
Global Interpreter Lock (GIL) in Python je zámek procesu nebo mutex používaný při práci s procesy. Zajišťuje, aby jedno vlákno mohlo přistupovat k určitému zdroji najednou, a také zabraňuje současnému použití objektů a bajtkódů. To prospívá jednovláknovým programům zvýšením výkonu. GIL v Python je velmi jednoduchý a snadno implementovatelný.
Zámek lze použít k zajištění toho, že pouze jedno vlákno má v daný čas přístup k určitému zdroji.
Jednou z funkcí Python je, že používá globální zámek na každý interpretační proces, což znamená, že každý proces zachází s Python samotný tlumočník jako zdroj.
Předpokládejme například, že jste napsali Python program, který používá dvě vlákna k provádění operací CPU i 'I/O'. Po spuštění tohoto programu se stane toto:
- Jedno Python Interpret vytvoří nový proces a spustí vlákna.
- Když vlákno-1 začne běžet, nejprve získá GIL a uzamkne jej.
- Pokud se vlákno-2 chce spustit nyní, bude muset počkat na uvolnění GIL, i když je volný jiný procesor.
- Nyní předpokládejme, že vlákno-1 čeká na I/O operaci. V tomto okamžiku uvolní GIL a vlákno-2 jej získá.
- Po dokončení I/O operací, pokud se vlákno-1 chce provést nyní, bude muset znovu počkat, až bude GIL uvolněno vláknem-2.
Z tohoto důvodu může k interpretu přistupovat pouze jedno vlákno v daném okamžiku, což znamená, že bude prováděno pouze jedno vlákno. Python kód v daném časovém okamžiku.
To je v pořádku u jednojádrového procesoru, protože by se pro zpracování vláken používalo časové rozdělení (viz první část tohoto tutoriálu). V případě vícejádrových procesorů však bude mít funkce vázaná na CPU, která se spouští na více vláknech, značný dopad na efektivitu programu, protože ve skutečnosti nebude využívat všechna dostupná jádra současně.
Proč byl GIL potřeba?
CPython Sběrač odpadků používá efektivní techniku správy paměti známou jako počítání referencí. Funguje to takto: Každý objekt v Python má počet referencí, který se zvyšuje, když je přiřazen novému názvu proměnné nebo přidán do kontejneru (jako jsou n-tice, seznamy atd.). Podobně se počet referencí snižuje, když reference překročí rozsah platnosti nebo když je volán příkaz del. Když počet referencí objektu dosáhne 0, je sklizen jako garbage collection a přidělená paměť je uvolněna.
Problém je ale v tom, že proměnná typu reference count je náchylná ke konfliktům soubojů stejně jako každá jiná globální proměnná. Aby tento problém vyřešili, vývojáři... Python rozhodli se použít globální zámek interpretu. Druhou možností bylo přidat zámek ke každému objektu, což by vedlo k zablokování a zvýšení režijních nákladů z volání acquire() a release().
GIL je proto významným omezením pro vícevláknové Python programy spouštějící náročné operace vázané na CPU (což je efektivně činí jednovláknovými). Pokud chcete ve své aplikaci využít více jader CPU, použijte multiprocessing modul místo toho.








