Vícevláknové v Python s Příkladem: Naučte se GIL v Python

Programovací jazyk python umožňuje používat multiprocessing nebo multithreading. V tomto tutoriálu se naučíte psát vícevláknové aplikace Python.

Co je vlákno?

Vlákno je jednotka provádění souběžného programování. Multithreading je technika, která umožňuje CPU provádět mnoho úloh jednoho procesu současně. Tato vlákna se mohou spouštět jednotlivě a zároveň sdílet své procesní prostředky.

Co je to proces?

Proces je v podstatě spuštěný program. Když spustíte aplikaci v počítači (jako prohlížeč nebo textový editor), operační systém vytvoří a proces.

V čem je multithreading Python?

Vícevláknové v Python programování je dobře známá technika, ve 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ě a zároveň sdílet své procesní prostředky. Účelem multithreadingu je spouštět více úloh a funkčních buněk 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

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

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

  • Kodex
  • Data

Nyní může proces obsahovat jednu nebo více podčástí nazývaných vlákna. Závisí to na architektuře operačního systému. O vláknu můžete uvažovat jako o části procesu, kterou může operační systém provádět samostatně.

Jinými slovy, je to 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 pro usnadnění paralelismu.

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 MultiThreading

Python podporuje konstrukce pro multiprocesing i multithreading. V tomto tutoriálu se zaměříte především na implementaci vícevláknové aplikace s pythonem. Existují dva hlavní moduly, které lze použít ke zpracování vláken Python:

  1. Jedno závit modulu a
  2. Jedno navlékání modul

V pythonu však existuje také něco, čemu se říká globální zámek interpretu (GIL). Neumožňuje velký nárůst 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é hodláte nasadit. Modul vlákna 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:

Závitový modul

Více o podmínkách závodu a jejich zvládnutí se dozvíte v následujících částech

Závitový modul

VYSVĚTLENÍ KÓDU

  1. Tyto příkazy importují modul času a vlákna, které se používají ke zpracování a zpoždění Python vlákna.
  2. Zde jste definovali funkci nazvanou test_vlákna, které bude volat start_new_thread metoda. Funkce spustí smyčku while po čtyři iterace a vypíše název vlákna, které ji volalo. Jakmile je iterace dokončena, vytiskne zprávu, že vlákno dokončilo provádění.
  3. Toto je hlavní část vašeho programu. Zde jednoduše zavoláte start_new_thread metoda s test_vlákna funkce jako argument. Tím se vytvoří nové vlákno pro funkci, kterou předáte jako argument, a spustí se její provádění. Všimněte si, že toto můžete nahradit (vlákno_test) s jakoukoli jinou funkcí, kterou chcete spustit jako vlákno.

Modul závitování

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

Struktura modulu Threading
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 kódovat programy s více vlákny pomocí modulu vláken, je důležité porozumět třídě Thread. Třída vláken je primární třída, která definuje šablonu a operace vlákna v pythonu.

Nejběžnějším způsobem, jak vytvořit vícevláknovou aplikaci pythonu, 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í:

  1. definovat třídu, která rozšiřuje třídu Thread
  2. Přepsat __init__ konstruktérem
  3. Přepsat běh() metoda

Jakmile byl vytvořen objekt vlákna, Start() metodu lze použít k zahájení provádění této činnosti 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 vláken k implementaci vašeho předchozího příkladu. Znovu, zapalte svůj 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:

Příběh: The Thread Class

VYSVĚTLENÍ KÓDU

Příběh: The Thread Class

  1. Tato část je stejná jako náš předchozí příklad. Zde importujete modul času a vlákna, které se používají ke zpracování provádění a zpoždění Python vlákna.
  2. 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 závitování. Toto je jeden z nejběžnějších způsobů vytváření vláken v pythonu. Mě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().
  3. Toto je metoda thread_test(), která přebírá hodnotu i jako argument jej při každé iteraci sníží o 1 a prochází zbytkem 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í čekací sekundy (což je také bráno jako argument ).
  4. 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.
  5. thread2.start()T metoda start se používá ke spuštění provádění vlákna. Interně funkce start() volá metodu run() vaší třídy.
  6. 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 pokouší změnit nebo přistupovat k datům současně, mohou se vloudit chyby.

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

Zablokování a závodní podmínky

Než se seznámíte s patovými situacemi a podmínkami závodu, bude užitečné porozumět několika základním definicím souvisejícím se souběžným programováním:

  • Critical SectionIt je fragment kódu, který přistupuje nebo upravuje sdílené proměnné a musí být proveden jako atomická transakce.
  • Přepínání kontextu Je to proces, který CPU sleduje, aby uložil stav vlákna před přechodem z jedné úlohy na druhou, aby mohl být později obnoven ze stejného bodu.

Zablokování

Zablokování jsou 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 pomocí klasického příkladu počítačové vědy známého 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

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

Spor 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 tento jednoduchý cyklus for:

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

Pokud vytvoříte n počet vláken, která spouštějí 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 tím změnit, když k ní přistoupí nějaké jiné vlákno.

Toto jsou dvě hlavní třídy problémů, které se mohou vyskytnout ve vícevláknové nebo distribuované pythonové aplikaci. 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. V každém okamžiku může být zámek v jednom ze 2 stavů: zamčený or odemčený. Podporuje dvě metody:

  1. získat()Když je lock-state odemčeno, volání metody purchase() změní stav na locked a vrátí se. Pokud je však stav uzamčen, volání metody purchase() je zablokováno, dokud není metoda release() volána jiným vláknem.
  2. 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. Zapalte svůj 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:

Synchronizující vlákna

VYSVĚTLENÍ KÓDU

Synchronizující vlákna

  1. 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.
  2. 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 fajn, ale jak pozná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 vždy přesně jeden řádek. Připomeňme si, že v předchozím příkladu byly výstupy z tisku nahodilé, protože k metodě print() přistupovalo současně více vláken. Zde se funkce tisku vyvolá až po získání zámku. Výstupy se tedy 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. RLocks
  2. Semaphores
  3. Podmínky
  4. Události a
  5. Bariéry

Global Interpreter Lock (a jak se s tím vypořádat)

Než se dostaneme do podrobností o GIL pythonu, pojďme definovat několik termínů, které budou užitečné pro pochopení nadcházející sekce:

  1. Kód vázaný na CPU: odkazuje na jakýkoli kus kódu, který bude přímo spuštěn CPU.
  2. I/O-bound kód: může to být jakýkoli kód, který přistupuje k systému souborů přes OS
  3. 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) v pythonu je zámek procesu nebo mutex používaný při práci s procesy. Zajišťuje, že jedno vlákno může přistupovat k určitému zdroji najednou, a také zabraňuje použití objektů a bajtkódů najednou. To přináší výhody jednovláknovým programům ve zvýšení výkonu. GIL v pythonu 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 spočívá v tom, že používá globální zámek na každý proces tlumočníka, což znamená, že každý proces zachází se samotným pythonovým tlumočníkem jako se zdrojem.

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

  1. Pythonový interpret vytvoří nový proces a vytvoří vlákna
  2. Když vlákno-1 začne běžet, nejprve získá GIL a uzamkne jej.
  3. Pokud se vlákno-2 chce spustit nyní, bude muset počkat na uvolnění GIL, i když je volný jiný procesor.
  4. 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á.
  5. 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 interpretru kdykoli přistupovat pouze jedno vlákno, což znamená, že v daném okamžiku bude existovat pouze jedno vlákno spouštějící kód pythonu.

To je v pořádku v jednojádrovém procesoru, protože by ke zpracování vláken používal časové dělení (viz první část tohoto návodu). V případě vícejádrových procesorů však funkce vázaná na CPU spouštěná na více vláknech bude mít 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 garbage collector využí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 (jako jsou n-tice, seznamy atd.). Podobně se počet odkazů sníží, když odkaz překročí rozsah nebo když je zavolán příkaz del. Když počet referencí objektu dosáhne 0, je shromážděn odpad a přidělená paměť se uvolní.

Problém je ale v tom, že proměnná počtu referencí je náchylná k závodům jako každá jiná globální proměnná. K vyřešení tohoto problému se vývojáři pythonu rozhodli použít globální zámek interpretu. Druhou možností bylo přidat zámek ke každému objektu, což by vedlo k uváznutí a zvýšené režii volání accept() a release().

Proto je GIL významným omezením pro vícevláknové pythonové programy provozující těžké operace vázané na CPU (efektivně je činí jednovláknovými). Pokud chcete ve své aplikaci využít více jader CPU, použijte multiprocessing modul místo toho.

Shrnutí

  • Python podporuje 2 moduly pro multithreading:
    1. __vlákno modul: Poskytuje nízkoúrovňovou implementaci pro vytváření vláken a je zastaralý.
    2. závitový modul: Poskytuje implementaci na vysoké úrovni pro multithreading a je současným standardem.
  • Chcete-li vytvořit vlákno pomocí modulu pro vytváření vláken, musíte provést následující:
    1. Vytvořte třídu, která rozšiřuje Vlákno třída.
    2. Přepište jeho konstruktor (__init__).
    3. Přepsat jeho běh() metoda.
    4. Vytvořte objekt této třídy.
  • Vlákno lze spustit voláním Start() metoda.
  • Jedno připojit se() metodu lze použít k blokování dalších vláken, dokud toto vlákno (to, na kterém bylo zavoláno spojení) nedokončí provádění.
  • Spor nastane, když více podprocesů přistupuje nebo upravuje sdílený prostředek současně.
  • Dá se tomu vyhnout Synchronizující vlákna.
  • Python podporuje 6 způsobů synchronizace vláken:
    1. Zámky
    2. RLocks
    3. Semaphores
    4. Podmínky
    5. Události a
    6. Bariéry
  • Zámky umožňují vstoupit do kritické sekce pouze konkrétnímu vláknu, které získalo zámek.
  • Zámek má 2 primární metody:
    1. získat(): Nastaví stav uzamčení na uzamčen. Pokud je zavolán na uzamčený objekt, zablokuje se, dokud není zdroj uvolněn.
    2. uvolnění(): Nastaví stav uzamčení na odemčený a vrací se. Pokud je zavolán na odemčený objekt, vrátí false.
  • Globální zámek tlumočníka je mechanismus, jehož prostřednictvím pouze 1 CPython proces tlumočníka lze spustit najednou.
  • Byl použit k usnadnění funkce počítání referencí CPythons je popelář.
  • Chcete-li Python aplikace s náročnými operacemi vázanými na CPU, měli byste použít modul pro více zpracování.