Multithreading-in Python met Voorbeeld: Leer GIL in Python
Wat is een draad?
Een thread is een uitvoeringseenheid bij gelijktijdig programmeren. Multithreading is een techniek waarmee een CPU veel taken van één proces tegelijkertijd kan uitvoeren. Deze threads kunnen afzonderlijk worden uitgevoerd terwijl ze hun procesbronnen delen.
Wat is een proces?
Een proces is in principe het programma in uitvoering. Wanneer u een applicatie start op uw computer (zoals een browser of teksteditor), creëert het besturingssysteem een proces.
Wat is multithreading? Python?
Multithreading-in Python Programmeren is een bekende techniek waarbij meerdere threads in een proces hun dataruimte delen met de hoofdthread, waardoor het delen van informatie en communicatie binnen threads eenvoudig en efficiënt wordt. Draden zijn lichter dan processen. Meerdere threads kunnen afzonderlijk worden uitgevoerd terwijl hun procesbronnen worden gedeeld. Het doel van multithreading is om meerdere taken en functiecellen tegelijkertijd uit te voeren.
Wat is multiprocessing?
multiprocessing stelt u in staat om meerdere ongerelateerde processen tegelijkertijd uit te voeren. Deze processen delen hun bronnen niet en communiceren via IPC.
Python Multithreading versus multiprocessing
Om processen en threads te begrijpen, kunt u het volgende scenario overwegen: Een .exe-bestand op uw computer is een programma. Wanneer u het opent, laadt het besturingssysteem het in het geheugen en voert de CPU het uit. De instantie van het programma dat nu wordt uitgevoerd, wordt het proces genoemd.
Elk proces zal twee fundamentele componenten hebben:
- De code
- De data
Nu kan een proces een of meer zogenaamde subonderdelen bevatten threads. Dit is afhankelijk van de architectuur van het besturingssysteem. U kunt een thread beschouwen als een onderdeel van het proces dat afzonderlijk door het besturingssysteem kan worden uitgevoerd.
Met andere woorden, het is een stroom instructies die onafhankelijk door het besturingssysteem kan worden uitgevoerd. Threads binnen een enkel proces delen de gegevens van dat proces en zijn ontworpen om samen te werken om parallellisme te faciliteren.
Waarom multithreading gebruiken?
Met multithreading kunt u een applicatie opsplitsen in meerdere subtaken en deze taken tegelijkertijd uitvoeren. Als u multithreading op de juiste manier gebruikt, kunnen de snelheid, prestaties en rendering van uw applicatie worden verbeterd.
Python MultiThreading
Python ondersteunt constructies voor zowel multiprocessing als multithreading. In deze tutorial richt u zich voornamelijk op het implementeren meerdradig toepassingen met python. Er zijn twee hoofdmodules die gebruikt kunnen worden om threads te verwerken in Python:
- Het draad module, en
- Het threading module
In Python bestaat er echter ook zoiets als een global interpreter lock (GIL). Het zorgt niet voor veel prestatiewinst en misschien zelfs wel verminderen de prestaties van sommige multithreaded applicaties. Je leert er alles over in de komende secties van deze tutorial.
De modules Draad en Draadsnijden
De twee modules waarover u in deze zelfstudie leert, zijn de draadmodule en draadmodule.
De threadmodule is echter al lang verouderd. Te beginnen met Python 3, is het als verouderd aangemerkt en is het alleen toegankelijk als __draad voor achterwaartse compatibiliteit.
Je moet het hogere niveau gebruiken threading module voor applicaties die u wilt inzetten. De draadmodule wordt hier alleen behandeld voor educatieve doeleinden.
De draadmodule
De syntaxis voor het maken van een nieuwe thread met deze module is als volgt:
thread.start_new_thread(function_name, arguments)
Oké, nu heb je de basistheorie behandeld om te beginnen met coderen. Open dus uw IDLE of een kladblok en typ het volgende:
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))
Sla het bestand op en druk op F5 om het programma uit te voeren. Als alles correct is gedaan, is dit de uitvoer die u zou moeten zien:
In de komende secties leer je meer over raceomstandigheden en hoe je hiermee om moet gaan
CODE-UITLEG
- Deze instructies importeren de tijd- en threadmodule die worden gebruikt om de uitvoering en vertraging van de Python threads.
- Hier hebt u een functie gedefinieerd met de naam draad_test, die zal worden gebeld door de start_nieuwe_thread methode. De functie voert een while-lus uit gedurende vier iteraties en drukt de naam af van de thread die deze heeft aangeroepen. Zodra de iteratie is voltooid, wordt er een bericht afgedrukt waarin staat dat de uitvoering van de thread is voltooid.
- Dit is het hoofdgedeelte van je programma. Hier belt u eenvoudigweg de start_nieuwe_thread methode met de draad_test function als argument. Hierdoor wordt een nieuwe thread gemaakt voor de functie die u als argument doorgeeft en wordt deze uitgevoerd. Merk op dat u dit kunt vervangen (thread_test) met een andere functie die u als thread wilt uitvoeren.
De draadmodule
Deze module is de implementatie op hoog niveau van threading in Python en de de facto standaard voor het beheren van multithreaded applicaties. Het biedt een breed scala aan functies in vergelijking met de draadmodule.
Hier is een lijst met enkele nuttige functies die in deze module zijn gedefinieerd:
Functie Naam | Beschrijving |
---|---|
actieveAantal() | Retourneert het aantal van Draad voorwerpen die nog leven |
huidigeDraad() | Retourneert het huidige object van de klasse Thread. |
opsommen () | Geeft een overzicht van alle actieve Thread-objecten. |
isDaemon() | Retourneert true als de thread een daemon is. |
is levend() | Retourneert waar als de thread nog leeft. |
Thread Class-methoden | |
begin() | Start de activiteit van een thread. Het moet voor elke thread slechts één keer worden aangeroepen, omdat er een runtimefout ontstaat als het meerdere keren wordt aangeroepen. |
rennen() | Deze methode geeft de activiteit van een thread aan en kan worden overschreven door een klasse die de Thread-klasse uitbreidt. |
meedoen () | Het blokkeert de uitvoering van andere code totdat de thread waarop de methode join() werd aangeroepen, wordt beëindigd. |
Achtergrondverhaal: de draadklasse
Voordat u begint met het coderen van multithreaded programma's met behulp van de threadingmodule, is het van cruciaal belang dat u de Thread-klasse begrijpt. De Thread-klasse is de primaire klasse die de sjabloon en de bewerkingen van een thread in Python definieert.
De meest gebruikelijke manier om een multithreaded Python-toepassing te maken, is door een klasse te declareren die de Thread-klasse uitbreidt en de run() -methode overschrijft.
Samenvattend betekent de Thread-klasse een codereeks die in een afzonderlijke codereeks wordt uitgevoerd draad van controle.
Wanneer u een multithreaded app schrijft, doet u het volgende:
- definieer een klasse die de Thread-klasse uitbreidt
- Overschrijf de __in het__ aannemer
- Overschrijf de rennen() methode
Zodra een draadobject is gemaakt, wordt het begin() methode kan worden gebruikt om te beginnen met de uitvoering van deze activiteit en de meedoen () methode kan worden gebruikt om alle andere code te blokkeren totdat de huidige activiteit is voltooid.
Laten we nu proberen de threadingmodule te gebruiken om uw vorige voorbeeld te implementeren. Nogmaals, start uw IDLE en typ het volgende in:
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()
Dit zal de uitvoer zijn wanneer u de bovenstaande code uitvoert:
CODE-UITLEG
- Dit onderdeel is hetzelfde als ons vorige voorbeeld. Hier importeert u de time- en threadmodule die worden gebruikt om de uitvoering en vertragingen van de Python threads.
- In dit bit maak je een klasse met de naam threadtester, die de Draad klasse van de draadsnijmodule. Dit is een van de meest gebruikelijke manieren om threads in Python te maken. U moet echter alleen de constructor en de rennen() methode in uw app. Zoals je in het bovenstaande codevoorbeeld kunt zien, is de __in het__ methode (constructor) is overschreven. Op dezelfde manier heb je ook de rennen() methode. Het bevat de code die u in een thread wilt uitvoeren. In dit voorbeeld heb je de functie thread_test() aangeroepen.
- Dit is de thread_test() methode die de waarde van aanneemt i als argument, verlaagt het met 1 bij elke iteratie en doorloopt de rest van de code totdat i 0 wordt. In elke iteratie wordt de naam van de momenteel uitgevoerde thread afgedrukt en wacht enkele seconden in de slaapstand (wat ook als argument wordt opgevat ).
- thread1 = threadtester(1, “Eerste Thread”, 1) Hier maken we een thread en geven we de drie parameters door die we hebben aangegeven in __init__. De eerste parameter is de ID van de thread, de tweede parameter is de naam van de thread en de derde parameter is de teller, die bepaalt hoe vaak de while-lus moet worden uitgevoerd.
- thread2.start()De startmethode wordt gebruikt om de uitvoering van een thread te starten. Intern roept de functie start() de methode run() van uw klasse aan.
- thread3.join() De methode join() blokkeert de uitvoering van andere code en wacht tot de thread waarop deze is aangeroepen, is voltooid.
Zoals u al weet, hebben de threads die zich in hetzelfde proces bevinden toegang tot het geheugen en de data van dat proces. Als gevolg hiervan kunnen er fouten insluipen als meer dan één thread tegelijkertijd probeert de data te wijzigen of te benaderen.
In de volgende sectie ziet u de verschillende soorten complicaties die kunnen optreden wanneer threads toegang krijgen tot gegevens en kritieke secties zonder te controleren op bestaande toegangstransacties.
Deadlocks en raceomstandigheden
Voordat we meer leren over deadlocks en raceomstandigheden, is het handig om een paar basisdefinities met betrekking tot gelijktijdige programmering te begrijpen:
- Kritieke sectieDit is een codefragment dat toegang heeft tot gedeelde variabelen of deze wijzigt. Dit moet worden uitgevoerd als een atomaire transactie.
- Context SwitchDit is het proces dat een CPU volgt om de status van een thread op te slaan voordat deze van de ene taak naar de andere overschakelt, zodat de taak later vanaf hetzelfde punt kan worden hervat.
impasses
impasses zijn het meest gevreesde probleem waar ontwikkelaars mee te maken krijgen bij het schrijven van gelijktijdige/multithreaded applicaties in Python. De beste manier om deadlocks te begrijpen is door het klassieke computerwetenschapsvoorbeeldprobleem te gebruiken dat bekendstaat als de Dineren Philosofers Probleem.
De probleemstelling voor eetfilosofen luidt als volgt:
Vijf filosofen zitten aan een ronde tafel met vijf borden spaghetti (een soort pasta) en vijf vorken, zoals te zien is op de tekening.
Een filosoof moet op elk willekeurig moment óf eten óf denken.
Bovendien moet een filosoof de twee vorken die naast hem liggen (d.w.z. de linker- en rechtervork) pakken voordat hij de spaghetti kan eten. Het probleem van deadlock doet zich voor wanneer alle vijf filosofen tegelijkertijd hun rechtervork oppakken.
Omdat elke filosofen één vork heeft, zullen ze allemaal wachten tot de anderen hun vork neerleggen. Als gevolg hiervan zal niemand van hen spaghetti kunnen eten.
Op dezelfde manier treedt er in een gelijktijdig systeem een deadlock op wanneer verschillende threads of processen (philosophers) tegelijkertijd proberen de gedeelde systeembronnen (forks) te verwerven. Als gevolg hiervan krijgt geen van de processen de kans om uit te voeren, omdat ze wachten op een andere bron die door een ander proces wordt vastgehouden.
Race voorwaarden
Een raceconditie is een ongewenste toestand van een programma die optreedt wanneer een systeem twee of meer bewerkingen tegelijk uitvoert. Beschouw bijvoorbeeld deze eenvoudige for-lus:
i=0; # a global variable for x in range(100): print(i) i+=1;
Als je creëert n Als er meer threads zijn die deze code tegelijk uitvoeren, kunt u de waarde van i (die door de threads wordt gedeeld) niet bepalen wanneer de uitvoering van het programma is voltooid. Dit komt omdat in een echte multithreading-omgeving de threads elkaar kunnen overlappen, en de waarde van i die door een thread is opgehaald en gewijzigd, tussendoor kan veranderen wanneer een andere thread er toegang toe krijgt.
Dit zijn de twee belangrijkste klassen van problemen die kunnen optreden in een multithreaded of gedistribueerde python-applicatie. In de volgende sectie leert u hoe u dit probleem kunt oplossen door threads te synchroniseren.
Syncroniserende draden
Om met race-omstandigheden, deadlocks en andere thread-gebaseerde problemen om te gaan, biedt de threading-module de volgende mogelijkheden: Slot object. Het idee is dat wanneer een thread toegang wil tot een specifieke resource, deze een lock voor die resource verkrijgt. Zodra een thread een bepaalde resource lockt, kan geen enkele andere thread er toegang toe krijgen totdat de lock wordt vrijgegeven. Als gevolg hiervan zullen de wijzigingen aan de resource atomisch zijn en worden racecondities afgewend.
Een slot is een synchronisatieprimitief op laag niveau dat wordt geïmplementeerd door de __draad module. Een slot kan zich op elk moment in een van de volgende twee toestanden bevinden: opgesloten or ontgrendeld. Het ondersteunt twee methoden:
- verwerven()Wanneer de lock-state ontgrendeld is, zal het aanroepen van de acquire()-methode de status veranderen in vergrendeld en terugkeren. Als de status echter vergrendeld is, wordt de aanroep van acquire() geblokkeerd totdat de methode release() door een andere thread wordt aangeroepen.
- uitgave()De release()-methode wordt gebruikt om de status op ontgrendeld te zetten, dat wil zeggen om een vergrendeling op te heffen. Het kan door elke thread worden aangeroepen, niet noodzakelijkerwijs degene die het slot heeft verkregen.
Hier is een voorbeeld van het gebruik van vergrendelingen in uw apps. Start uw IDLE en typ het volgende:
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()
Druk nu op F5. Je zou een uitvoer als deze moeten zien:
CODE-UITLEG
- Hier maakt u eenvoudigweg een nieuw slot aan door de draadsnijden.Lock() fabrieksfunctie. Intern retourneert Lock() een instantie van de meest effectieve concrete Lock-klasse die door het platform wordt onderhouden.
- In de eerste instructie verkrijgt u het slot door de methode acquire() aan te roepen. Wanneer de vergrendeling is verleend, drukt u af “slot verworven” naar de console. Zodra alle code die u door de thread wilt laten uitvoeren, is uitgevoerd, geeft u de vergrendeling vrij door de methode release() aan te roepen.
De theorie is prima, maar hoe weet je dat de lock echt werkte? Als je naar de output kijkt, zie je dat elk van de print statements precies één regel tegelijk print. Herinner je dat in een eerder voorbeeld de outputs van print willekeurig waren omdat meerdere threads tegelijkertijd de print() methode benaderden. Hier wordt de print functie pas aangeroepen nadat de lock is verkregen. Dus de outputs worden één voor één en regel voor regel weergegeven.
Naast sloten ondersteunt Python ook een aantal andere mechanismen om threadsynchronisatie af te handelen, zoals hieronder vermeld:
- RSloten
- Semaphores
- Klachten
- Evenementen, en
- Barrières
Global Interpreter Lock (en hoe hiermee om te gaan)
Voordat we dieper ingaan op de GIL van Python, definiëren we eerst een aantal termen die handig zijn om het komende gedeelte te begrijpen:
- CPU-gebonden code: dit verwijst naar elk stukje code dat rechtstreeks door de CPU wordt uitgevoerd.
- I/O-gebonden code: dit kan elke code zijn die via het besturingssysteem toegang heeft tot het bestandssysteem
- CPython: het is de referentie uitvoering of Python en kan worden omschreven als de tolk geschreven in C en Python (programmeertaal).
Waar zit GIL in? Python?
Wereldwijd tolkslot (GIL) in Python wordt een procesvergrendeling of een mutex gebruikt bij het omgaan met de processen. Het zorgt ervoor dat één thread tegelijk toegang heeft tot een bepaalde bron en voorkomt ook het gebruik van objecten en bytecodes tegelijk. Dit komt de single-threaded programma's ten goede in een prestatieverbetering. GIL in Python is heel eenvoudig en gemakkelijk te implementeren.
Een vergrendeling kan worden gebruikt om ervoor te zorgen dat slechts één thread op een bepaald moment toegang heeft tot een bepaalde bron.
Een van de kenmerken van Python is dat het een globale vergrendeling gebruikt op elk interpreterproces, wat betekent dat elk proces de Python-interpreter zelf als een resource behandelt.
Stel bijvoorbeeld dat u een python-programma hebt geschreven dat twee threads gebruikt om zowel CPU- als 'I/O'-bewerkingen uit te voeren. Wanneer u dit programma uitvoert, gebeurt het volgende:
- De Python-interpreter creëert een nieuw proces en brengt de threads voort
- Wanneer thread-1 begint te lopen, zal het eerst de GIL verkrijgen en vergrendelen.
- Als thread-2 nu wil uitvoeren, zal het moeten wachten tot de GIL wordt vrijgegeven, zelfs als er een andere processor vrij is.
- Stel nu dat thread-1 wacht op een I/O-bewerking. Op dat moment zal het de GIL vrijgeven en thread-2 zal deze verkrijgen.
- Als thread-1 na het voltooien van de I/O-bewerkingen nu wil worden uitgevoerd, zal het opnieuw moeten wachten tot de GIL door thread-2 wordt vrijgegeven.
Hierdoor heeft slechts één thread op elk moment toegang tot de tolk, wat betekent dat er op een bepaald tijdstip slechts één thread is die Python-code uitvoert.
Dit is prima in een single-coreprocessor, omdat deze time-slicing zou gebruiken (zie het eerste gedeelte van deze tutorial) om de threads af te handelen. In het geval van multi-core processors zal een CPU-gebonden functie die op meerdere threads wordt uitgevoerd echter een aanzienlijke impact hebben op de efficiëntie van het programma, aangezien het niet daadwerkelijk alle beschikbare cores tegelijkertijd zal gebruiken.
Waarom was GIL nodig?
de CPython garbage collector gebruikt een efficiënte geheugenbeheertechniek die bekend staat als referentietelling. Dit is hoe het werkt: Elk object in python heeft een referentietelling, die wordt verhoogd wanneer het wordt toegewezen aan een nieuwe variabelenaam of wordt toegevoegd aan een container (zoals tuples, lijsten, enz.). Op dezelfde manier wordt de referentietelling verlaagd wanneer de referentie buiten bereik raakt of wanneer de del-instructie wordt aangeroepen. Wanneer de referentietelling van een object 0 bereikt, wordt het object door garbage collector verzameld en wordt het toegewezen geheugen vrijgegeven.
Maar het probleem is dat de referentietellingvariabele gevoelig is voor racecondities, net als elke andere globale variabele. Om dit probleem op te lossen, besloten de ontwikkelaars van Python om de globale interpreter lock te gebruiken. De andere optie was om een lock toe te voegen aan elk object, wat zou hebben geresulteerd in deadlocks en verhoogde overhead van acquire() en release() calls.
Daarom is GIL een belangrijke beperking voor multithreaded python-programma's die zware CPU-gebonden bewerkingen uitvoeren (waardoor ze effectief single-threaded worden). Als u meerdere CPU-cores in uw toepassing wilt gebruiken, gebruikt u de multiverwerking module in plaats daarvan.
Samenvatting
- Python ondersteunt 2 modules voor multithreading:
- __draad module: Het biedt een implementatie op laag niveau voor threading en is verouderd.
- draadmodule: Het biedt een implementatie op hoog niveau voor multithreading en is de huidige standaard.
- Om een thread te maken met behulp van de threadingmodule, moet u het volgende doen:
- Maak een klasse die de extensie uitbreidt Draad klasse.
- Overschrijf de constructor ervan (__init__).
- Overschrijf het rennen() methode.
- Maak een object van deze klasse.
- Een thread kan worden uitgevoerd door de begin() methode.
- Het meedoen () methode kan worden gebruikt om andere threads te blokkeren totdat deze thread (degene waarop join werd aangeroepen) de uitvoering voltooit.
- Er treedt een race condition op wanneer meerdere threads tegelijkertijd toegang hebben tot een gedeelde bron of deze wijzigen.
- Het kan vermeden worden door Syncroniserende draden.
- Python ondersteunt 6 manieren om threads te synchroniseren:
- Sloten
- RSloten
- Semaphores
- Klachten
- Evenementen, en
- Barrières
- Sloten laten alleen een bepaalde draad toe die het slot heeft verworven om het kritieke gedeelte binnen te gaan.
- Een slot heeft 2 primaire methoden:
- verwerven(): Hiermee wordt de vergrendelingsstatus ingesteld op slot. Als een vergrendeld object wordt aangeroepen, blokkeert het totdat de bron vrij is.
- uitgave(): Hiermee wordt de vergrendelingsstatus ingesteld ontgrendeld en keert terug. Als een ontgrendeld object wordt aangeroepen, retourneert het false.
- Het global interpreter lock is een mechanisme waardoor slechts 1 CPython tolkproces kan tegelijkertijd worden uitgevoerd.
- Het werd gebruikt om de referentietellingsfunctionaliteit van CPythons'vuilnisman.
- Te maken Python Voor apps met zware CPU-belastende bewerkingen kunt u het beste de multiprocessingmodule gebruiken.