Vícevláknové zpracování v Pythonu s příkladem Global Interpreter Lock (GIL)

Obsah:

Anonim

Programovací jazyk pythonu vám umožňuje používat multiprocesing nebo multithreading. V tomto kurzu se naučíte psát vícevláknové aplikace v Pythonu.

Co je vlákno?

Vlákno je jednotka exekce při souběžném programování. Multithreading je technika, která umožňuje CPU vykonávat mnoho úkolů jednoho procesu současně. Tato vlákna se mohou spouštět jednotlivě při sdílení jejich procesních prostředků.

Co je to proces?

Proces je v podstatě prováděný program. Když spustíte aplikaci v počítači (například prohlížeč nebo textový editor), vytvoří operační systém proces.

Co je to multithreading v Pythonu?

Multithreading v programování v Pythonu 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 jednotlivě při sdílení jejich procesních prostředků. Účelem multithreadingu je spouštět více úkolů a funkčních buněk současně.

Co je to multiprocesing?

Multiprocesing vám umožňuje spouštět více nesouvisejících procesů současně. Tyto procesy nesdílejí své zdroje a komunikují prostřednictvím IPC.

Python Multithreading vs. Multiprocessing

Chcete-li porozumět procesům a podprocesům, zvažte tento scénář: Soubor .exe ve vašem počítači je program. Když ji otevřete, OS ji načte do paměti a CPU ji provede. Instance programu, který je nyní spuštěn, se nazývá proces.

Každý proces bude mít 2 základní komponenty:

  • Kód
  • Data

Proces nyní může obsahovat jednu nebo více dílčích částí zvaných vlákna. To záleží na OS architektuře .Můžete myslet na vlásku jako části procesu, který může být proveden odděleně od operačního systému.

Jinými slovy, jedná se o proud instrukcí, které lze OS spustit nezávisle. Vlákna v rámci jednoho procesu sdílejí data tohoto procesu a jsou navržena tak, aby spolupracovala na usnadnění paralelismu.

V tomto výukovém programu se naučíte,

  • Co je vlákno?
  • Co je to proces?
  • Co je to multithreading?
  • Co je to multiprocesing?
  • Python Multithreading vs. Multiprocessing
  • Proč používat Multithreading?
  • Python MultiThreading
  • Vlákno a vlákna moduly
  • Vláknový modul
  • Vláknový modul
  • Zablokování a podmínky závodu
  • Synchronizace vláken
  • Co je GIL?
  • Proč byl GIL potřebný?

Proč používat Multithreading?

Vícevláknové zpracování umožňuje rozdělit aplikaci na více dílčích úkolů a spouštět tyto úkoly současně. Pokud používáte vícevláknové zpracování správně, lze zlepšit rychlost, výkon a vykreslování vaší aplikace.

Python MultiThreading

Python podporuje konstrukce jak pro multiprocesing, tak pro multithreading. V tomto kurzu se primárně zaměříte na implementaci vícevláknových aplikací s pythonem. Existují dva hlavní moduly, které lze použít ke zpracování vláken v Pythonu:

  1. Modul vlákna a
  2. Threading modul

V pythonu však existuje také něco, co se nazývá zámek globálního tlumočníka (GIL). Neumožňuje velké zvýšení výkonu a může dokonce snížit výkon některých vícevláknových aplikací. Dozvíte se vše o tom v následujících částech tohoto tutoriálu.

Vlákno a vlákna moduly

Dva moduly, o kterých se v tomto kurzu dozvíte, jsou modul vlákna a modul vláken .

Modul vlákna je však již dlouho zastaralý. Počínaje Pythonem 3 byl označen jako zastaralý a pro zpětnou kompatibilitu je přístupný pouze jako __thread .

Modul threading vyšší úrovně byste měli používat pro aplikace, které hodláte nasadit. Vláknový modul zde byl uveden pouze pro vzdělávací účely.

Vláknový 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 se zabývali základní teorií zahájení kódování. Otevřete tedy IDLE nebo poznámkový blok a zadejte následující:

import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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 stisknutím klávesy F5 spusťte program. Pokud bylo vše provedeno správně, měli byste vidět tento výstup:

Více o podmínkách závodu a o tom, jak s nimi zacházet, se dozvíte v následujících částech

VYSVĚTLENÍ KÓDU

  1. Tyto příkazy importují modul času a vlákna, který se používá ke zpracování provádění a zpoždění vláken Pythonu.
  2. Zde jste definovali funkci nazvanou thread_test, která bude volána metodou start_new_thread . Funkce spustí smyčku while pro čtyři iterace a vytiskne název vlákna, které ji volalo. Po dokončení iterace vytiskne zprávu, že podproces dokončil provádění.
  3. Toto je hlavní část vašeho programu. Zde jednoduše zavoláte metodu start_new_thread s funkcí thread_test jako argument.

    Tím se vytvoří nové vlákno pro funkci, kterou předáte jako argument, a začne se provádět. Všimněte si, že můžete nahradit tento díl (nit _ test) s jakoukoli jinou funkci, kterou chcete spustit jako nit.

Vláknový modul

Tento modul je implementací vláken na vysoké úrovni v pythonu a de facto standardem pro správu vícevláknových aplikací. Poskytuje širokou škálu funkcí ve srovnání s vláknovým modulem.

Struktura modulu Threading

Zde je seznam některých užitečných funkcí definovaných v tomto modulu:

Název funkce Popis
activeCount () Vrátí počet objektů Thread, které jsou stále naživu
currentThread () Vrátí aktuální objekt třídy Thread.
vyjmenovat() Vypíše všechny aktivní objekty vláken.
isDaemon () Vrátí true, pokud je vlákno démon.
je naživu() Vrátí hodnotu true, pokud je vlákno stále naživu.
Metody třídy vláken
Start() Spustí aktivitu vlákna. Musí být volán pouze jednou pro každé vlákno, protože při opakovaném volání vyvolá runtime chybu.
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 volána metoda join ().

Backstory: Třída vlákna

Než začnete programovat vícevláknové programy pomocí modulu threading, je důležité pochopit třídu Thread. Třída thread je primární třída, která definuje šablonu a operace vlákna v pythonu.

Nejběžnějším způsobem, jak vytvořit aplikaci s více vlákny v pythonu, je deklarovat třídu, která rozšiřuje třídu Thread a přepíše její metodu run ().

Třída Thread v souhrnu označuje sekvenci kódu, která běží v samostatném vlákně kontroly.

Při psaní aplikace s více vlákny tedy uděláte toto:

  1. definovat třídu, která rozšiřuje třídu Thread
  2. Přepsat konstruktor __init__
  3. Potlačí run () metoda

Jakmile je objekt vlákna vytvořen, lze metodu start () použít k zahájení provádění této aktivity a metodu join () lze použít k blokování všech ostatních kódů, dokud nedojde k dokončení aktuální aktivity.

Nyní zkusme použít threading modul k implementaci vašeho předchozího příkladu. Znovu spusťte IDLE a zadejte následující:

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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()

Bude to výstup, když provedete výše uvedený kód:

VYSVĚTLENÍ KÓDU

  1. Tato část je stejná jako v předchozím příkladu. Zde importujete modul času a vlákna, který se používá ke zpracování provádění a zpoždění vláken Pythonu.
  2. V tomto bitu vytváříte třídu nazvanou threadtester, která dědí nebo rozšiřuje třídu Thread modulu threading. Toto je jeden z nejběžnějších způsobů vytváření vláken v pythonu. Měli byste však ve své aplikaci přepsat pouze konstruktor a metodu run () . Jak můžete vidět ve výše uvedeném ukázce kódu, metoda __init__ (konstruktor) byla přepsána.

    Podobně jste také přepsali metodu run () . Obsahuje kód, který chcete spustit uvnitř vlákna. V tomto příkladu jste volali funkci thread_test ().

  3. Toto je metoda thread_test (), která bere hodnotu i jako argument, snižuje ji o 1 při každé iteraci a prochází zbytkem kódu, dokud se i nestane 0. V každé iteraci vytiskne název aktuálně prováděného vlákna a spí na několik sekund (což je také bráno jako argument).
  4. thread1 = tester vláken (1, "první vlákno", 1)

    Zde vytváříme vlákno a předáváme tři parametry, které jsme deklarovali v __init__. První parametr je id vlákna, druhý parametr je název vlákna a třetí parametr je čítač, který určuje, kolikrát by měla být smyčka while spuštěna.

  5. 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.

  6. thread3.join ()

    Metoda join () blokuje provádění jiné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ýsledkem je, že pokud se více než jedno vlákno pokusí změnit nebo přistupovat k datům současně, mohou se dovnitř dostat chyby.

V další části uvidíte různé druhy komplikací, které se mohou projevit, když vlákna přistupují k datům a kritickým oddílům bez kontroly existujících přístupových transakcí.

Zablokování a podmínky závodu

Než se dozvíte o zablokování a podmínkách závodu, bude užitečné pochopit několik základních definic souvisejících se souběžným programováním:

  • Kritická sekce

    Jedná se o fragment kódu, který přistupuje nebo upravuje sdílené proměnné a musí být proveden jako atomická transakce.

  • Kontextový přepínač

    Jedná se o proces, který CPU sleduje, aby uložil stav vlákna před změnou z jednoho úkolu na jiný, aby jej bylo možné obnovit ze stejného bodu později.

Zablokování

Zablokování je nejobávanějším problémem, kterému vývojáři čelí při psaní souběžných / vícevláknových aplikací v pythonu. Nejlepší způsob, jak porozumět zablokování, je použití klasického příkladu počítačové vědy známého jako Problém jídelních filozofů.

Prohlášení problémových filosofů je následující:

Pět filozofů sedí na kulatém stole s pěti talíři špaget (druh těstovin) a pěti vidličkami, jak je znázorněno na obrázku.

Problém jídelních filozofů

Filozof musí v každém okamžiku buď jíst, nebo myslet.

Filosof musí navíc vzít dvě vidličky sousedící s ním (tj. Levou a pravou vidličku), než bude moci špagety sníst. Problém zablokování nastává, když všech pět filozofů zvedne pravou vidličku současně.

Protože každý z filozofů má jednu vidličku, budou všichni čekat, až ostatní vidličku odloží. Výsledkem je, že nikdo z nich nebude schopen jíst špagety.

Podobně v souběžném systému dochází k zablokování, když se různá vlákna nebo procesy (filozofové) pokusí získat sdílené systémové prostředky (rozvětvení) současně. Výsledkem je, že žádný z procesů nemá šanci na spuštění, protože čeká na jiný prostředek v držení jiného procesu.

Podmínky závodu

Podmínka závodu je nežádoucí stav programu, ke kterému dochází, když systém provádí dvě nebo více operací současně. Zvažte například tuto jednoduchou smyčku for:

i=0; # a global variablefor x in range(100):print(i)i+=1;

Pokud vytvoříte n počet vláken, která spouští tento kód najednou, nemůžete určit hodnotu i (která je sdílena vlákny), když program dokončí provádění. Je to proto, že ve skutečném prostředí s více vlákny se vlákna mohou překrývat a hodnota i, která byla načtena a upravena vláknem, se může mezi nimi změnit, když k němu přistupuje jiné vlákno.

Jedná se o dvě hlavní třídy problémů, které mohou nastat v multithreadové nebo distribuované aplikaci pythonu. V další části se naučíte, jak tento problém překonat synchronizací vláken.

Synchronizace vláken

Pro řešení podmínek závodu, zablokování a dalších problémů založených na podprocesech poskytuje modul podprocesů Lock objekt. Myšlenka je, že když vlákno chce přístup k určitému zdroji, získá zámek pro tento zdroj. Jakmile vlákno uzamkne konkrétní prostředek, žádné jiné vlákno k němu nebude mít přístup, dokud se zámek neuvolní. Výsledkem bude, že změny zdroje budou atomové a podmínky závodu budou odvráceny.

Zámek je primitivum synchronizace na nízké úrovni implementované modulem __thread . Zámek může být kdykoli v jednom ze 2 stavů: uzamčen nebo odemčen. Podporuje dvě metody:

  1. získat()

    Když je stav uzamčení odemčený, volání metody acquirování () změní stav na zamčený a vrátí se. Pokud je však stav uzamčen, volání na získání () je blokováno, dokud není metoda release () volána jiným vláknem.

  2. uvolnění()

    Metoda release () se používá k nastavení stavu na unlocked, tj. K uvolnění zámku. Může být volán 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 threadinglock = 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 klávesu F5. Měli byste vidět výstup, jako je tento:

VYSVĚTLENÍ KÓDU

  1. Tady jednoduše vytváříte nový zámek voláním tovární funkce threading.Lock () . Interně Lock () vrací instanci nejefektivnější konkrétní třídy Lock, která je udržována platformou.
  2. V prvním příkazu získáte zámek voláním metody acqui (). Když byl zámek udělen, vytisknete do konzoly text „zámek získán“ . Jakmile je dokončen veškerý kód, který má vlákno spustit, uvolníte zámek voláním metody release ().

Teorie je v pořádku, ale jak víte, že zámek opravdu fungoval? Pokud se podíváte na výstup, uvidíte, že každý z tiskových příkazů tiskne přesně jeden řádek najednou. Připomeňme, že v dřívějším příkladu byly výstupy z tisku nahodilé, protože k metodě print () přistupovalo současně více vláken. Zde je funkce tisku vyvolána až po získání zámku. Takže výstupy se zobrazují jeden po druhém a řádek po řádku.

Kromě zámků podporuje python také některé další mechanismy pro zpracování synchronizace vláken, jak je uvedeno níže:

  1. R Zámky
  2. Semafory
  3. Podmínky
  4. Události a
  5. Překážky

Globální tlumočník Lock (a jak s ním zacházet)

Než se pustíme do detailů pythonského GIL, definujme několik pojmů, které budou užitečné pro pochopení nadcházející sekce:

  1. Kód vázaný na CPU: to se týká jakéhokoli kódu, který bude přímo spuštěn CPU.
  2. Kód vázaný na I / O: může to být jakýkoli kód, který přistupuje k systému souborů přes OS
  3. CPython: jedná se o referenční implementaci Pythonu a lze jej popsat jako tlumočníka napsaného v jazycích C a Python (programovací jazyk).

Co je GIL v Pythonu?

Global Interpreter Lock (GIL) v pythonu je procesový zámek nebo mutex používaný při jednání s procesy. Zajišťuje, že jedno vlákno může přistupovat ke konkrétnímu zdroji najednou, a také brání použití objektů a bytových kódů najednou. To zvýhodňuje programy s jedním vláknem při zvyšování výkonu. GIL v pythonu je velmi jednoduchý a snadno implementovatelný.

Zámek lze použít k zajištění, že pouze jeden podproces má přístup k určitému prostředku v daném čase.

Jednou z funkcí Pythonu je, že používá globální zámek na každý proces tlumočníka, což znamená, že každý proces považuje samotného interpreta Pythonu za prostředek.

Předpokládejme například, že jste napsali program v pythonu, který používá dvě vlákna k provádění operací CPU i „I / O“. Když spustíte tento program, stane se toto:

  1. Interpret pythonu vytvoří nový proces a vytvoří vlákna
  2. Když vlákno 1 začne běžet, nejprve získá GIL a uzamkne ho.
  3. Pokud vlákno 2 chce provést nyní, bude muset počkat na vydání GIL, i když je volný další procesor.
  4. Nyní předpokládejme, že thread-1 čeká na I / O operaci. V tuto chvíli uvolní GIL a vlákno 2 jej získá.
  5. Po dokončení operací I / O, pokud chce vlákno-1 provést nyní, bude znovu muset počkat na vydání GIL vláknem-2.

Z tohoto důvodu může k interpretovi kdykoli přistupovat pouze jedno vlákno, což znamená, že v daném okamžiku bude existovat pouze jedno vlákno provádějící kód pythonu.

To je v pořádku u jednojádrového procesoru, protože by ke zpracování vláken používal časový segment (viz první část tohoto tutoriálu). Avšak v případě vícejádrových procesorů bude mít funkce vázaná na CPU spuštěná na více vláknech značný dopad na efektivitu programu, protože ve skutečnosti nebude používat všechna dostupná jádra současně.

Proč byl GIL potřebný?

Sběrač odpadků CPython používá efektivní techniku ​​správy paměti známou jako počítání referencí. Funguje to takto: Každý objekt v pythonu má počet odkazů, který se zvýší, když je přiřazen k novému názvu proměnné nebo přidán do kontejneru (například n-tice, seznamy atd.). Podobně se počet odkazů sníží, když odkaz zmizí z rozsahu nebo když se volá příkaz del. Když počet odkazů objektu dosáhne hodnoty 0, dojde k jeho uvolnění a uvolněná paměť se uvolní.

Ale problém je v tom, že proměnná počtu odkazů je náchylná k rasovým podmínkám jako každá jiná globální proměnná. K vyřešení tohoto problému se vývojáři pythonu rozhodli použít zámek globálního tlumočníka. Druhou možností bylo přidat zámek ke každému objektu, což by mělo za následek zablokování a zvýšenou režii z volání acqu () a release ().

Proto je GIL významným omezením pro vícevláknové programy v pythonu, které provozují těžké operace vázané na CPU (což je efektivně dělá s jedním vláknem). Pokud chcete ve své aplikaci využít více jader CPU, použijte místo toho víceprocesorový modul.

souhrn

  • Python podporuje 2 moduly pro multithreading:
    1. __thread module: Poskytuje implementaci na nízké úrovni pro threading a je zastaralá.
    2. modul threading : Poskytuje implementaci na vysoké úrovni pro multithreading a je aktuálním standardem.
  • Chcete-li vytvořit vlákno pomocí modulu vláken, musíte provést následující:
    1. Vytvořte třídu, která rozšiřuje třídu Thread .
    2. Přepsat jeho konstruktor (__init__).
    3. Přepsat jeho metodu run () .
    4. Vytvořte objekt této třídy.
  • Vlákno lze spustit voláním metody start () .
  • Připojit () metoda může být použita k blokování jiných vláken do tohoto závitu (na kterém spojit se nazývá) dokončení spuštění.
  • Spor nastane, když více vláken přistupuje nebo upravuje sdílený prostředek současně.
  • Tomu se lze vyhnout synchronizací vláken.
  • Python podporuje 6 způsobů synchronizace vláken:
    1. Zámky
    2. R Zámky
    3. Semafory
    4. Podmínky
    5. Události a
    6. Překážky
  • Zámky umožňují vstup do kritické sekce pouze určitému vláknu, které získalo zámek.
  • Zámek má 2 primární metody:
    1. acquir () : Nastaví stav zámku na zamčený. Pokud je vyvolán na uzamčeném objektu, blokuje se, dokud není prostředek volný.
    2. release () : Nastaví stav zámku na odemčený a vrátí se. Pokud je vyvolán odemčený objekt, vrátí hodnotu false.
  • Globální zámek tlumočníka je mechanismus, pomocí kterého lze spustit pouze 1 proces tlumočníka CPython.
  • To bylo použito k usnadnění funkce počítání referencí u CPythons's garbage collector.
  • Chcete-li vytvářet aplikace v Pythonu s těžkými operacemi vázanými na CPU, měli byste použít modul s více procesy.