De programmeertaal python stelt je in staat om multiprocessing of multithreading te gebruiken. In deze tutorial leer je hoe je multithreaded applicaties schrijft in Python.
Wat is een discussie?
Een thread is een eenheid van exectie op gelijktijdige programmering. Multithreading is een techniek waarmee een CPU veel taken van één proces tegelijkertijd kan uitvoeren. Deze threads kunnen afzonderlijk worden uitgevoerd terwijl ze hun procesresources delen.
Wat is een proces?
Een proces is in feite het programma dat wordt uitgevoerd. Wanneer u een toepassing op uw computer start (zoals een browser of teksteditor), creëert het besturingssysteem een proces.
Wat is multithreading in Python?
Multithreading in Python- programmering is een bekende techniek waarbij meerdere threads in een proces hun dataruimte delen met de hoofdthread, wat het delen van informatie en communicatie binnen threads eenvoudig en efficiënt maakt. Threads zijn lichter dan processen. Multi-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?
Met multiprocessing kunt u meerdere niet-gerelateerde processen tegelijkertijd uitvoeren. Deze processen delen hun bronnen niet en communiceren via IPC.
Python Multithreading versus multiprocessing
Overweeg dit scenario om processen en threads te begrijpen: 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 heeft 2 fundamentele componenten:
- De code
- De data
Nu kan een proces een of meer subonderdelen bevatten die threads worden genoemd. Dit hangt af van de OS-architectuur. U kunt een thread beschouwen als een onderdeel van het proces dat afzonderlijk kan worden uitgevoerd door het besturingssysteem.
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 vergemakkelijken.
In deze tutorial leer je,
- Wat is een discussie?
- Wat is een proces?
- Wat is multithreading?
- Wat is multiprocessing?
- Python Multithreading versus multiprocessing
- Waarom multithreading gebruiken?
- Python MultiThreading
- De modules Draad en Draadsnijden
- De draadmodule
- De inrijgmodule
- Deadlocks en race-omstandigheden
- Threads synchroniseren
- Wat is GIL?
- Waarom was GIL nodig?
Waarom multithreading gebruiken?
Met multithreading kunt u een applicatie opsplitsen in meerdere subtaken en deze taken tegelijkertijd uitvoeren. Als u multithreading correct gebruikt, kunnen uw applicatiesnelheid, prestaties en weergave allemaal worden verbeterd.
Python MultiThreading
Python ondersteunt constructies voor zowel multiprocessing als multithreading. In deze tutorial richt je je voornamelijk op het implementeren van multithreaded applicaties met python. Er zijn twee hoofdmodules die kunnen worden gebruikt om threads in Python af te handelen:
- De draadmodule , en
- De schroefdraad module
In python is er echter ook zoiets als een global interpreter lock (GIL). Het doet er niet toe dat voor een groot deel prestatiewinst en kan zelfs 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 die u in deze zelfstudie leert, zijn de threadmodule en de threadingmodule .
De threadmodule is echter al lang verouderd. Beginnend met Python 3, is het aangemerkt als verouderd en is het alleen toegankelijk als __thread voor achterwaartse compatibiliteit.
U moet de threading- module op een hoger niveau gebruiken voor toepassingen die u wilt implementeren. De draadmodule is hier alleen behandeld voor educatieve doeleinden.
De draadmodule
De syntaxis om een nieuwe thread te maken 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. Dus open je IDLE of een kladblok en typ het volgende in:
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))
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 racecondities 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 af te handelen.
- Hier heb je een functie gedefinieerd genaamd thread_test, die zal worden aangeroepen door de start_new_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 thread is voltooid.
- Dit is het hoofdgedeelte van uw programma. Hier roep je gewoon de start_new_thread- methode aan met de thread_test- functie als een argument.
Hiermee wordt een nieuwe thread gemaakt voor de functie die u als argument doorgeeft en begint deze uit te voeren. Merk op dat u deze (thread _ test) kunt vervangen door elke andere functie die u als thread wilt uitvoeren.
De inrijgmodule
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 handige functies die in deze module zijn gedefinieerd:
Functienaam | Omschrijving |
activeCount () | Retourneert het aantal Thread- objecten dat nog in leven is |
currentThread () | Retourneert het huidige object van de klasse Thread. |
opsommen () | Geeft een lijst van alle actieve Thread-objecten. |
isDaemon () | Geeft true terug als de thread een daemon is. |
is levend() | Geeft true terug als de thread nog in leven is. |
Thread Class-methoden | |
begin() | Start de activiteit van een thread. Het moet voor elke thread slechts één keer worden aangeroepen, omdat het een runtime-fout veroorzaakt 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 klasse Thread uitbreidt. |
toetreden () | Het blokkeert de uitvoering van andere code totdat de thread waarop de methode join () werd aangeroepen, wordt beëindigd. |
Achtergrondverhaal: The Thread Class
Voordat u begint met het coderen van multithreaded-programma's met behulp van de threading-module, is het cruciaal om de Thread-klasse te begrijpen. 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-applicatie te maken, is door een klasse te declareren die de Thread-klasse uitbreidt en de run () -methode overschrijft.
De Thread-klasse, samengevat, duidt een codereeks aan die wordt uitgevoerd in een aparte thread van controle.
Dus als u een app met meerdere threads schrijft, doet u het volgende:
- definieer een klasse die de Thread-klasse uitbreidt
- Overschrijf de __init__ constructor
- Overschrijf de methode run ()
Zodra een thread-object is gemaakt, kan de methode start () worden gebruikt om de uitvoering van deze activiteit te starten en kan de methode join () worden gebruikt om alle andere code te blokkeren totdat de huidige activiteit is voltooid.
Laten we nu proberen de threading-module te gebruiken om uw vorige voorbeeld te implementeren. Start opnieuw uw IDLE en typ het volgende in:
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()
Dit is de uitvoer wanneer u de bovenstaande code uitvoert:
CODE UITLEG
- Dit deel is hetzelfde als ons vorige voorbeeld. Hier importeert u de tijd- en threadmodule die worden gebruikt om de uitvoering en vertragingen van de Python-threads af te handelen.
- In dit bit maakt u een klasse met de naam threadtester, die de klasse Thread van de threading-module erft of uitbreidt . Dit is een van de meest gebruikelijke manieren om threads in python te maken. U moet echter alleen de constructor en de methode run () in uw app overschrijven . Zoals je kunt zien in het bovenstaande codevoorbeeld, is de __init__ methode (constructor) overschreven.
Op dezelfde manier hebt u ook de methode run () overschreven . Het bevat de code die u in een thread wilt uitvoeren. In dit voorbeeld heb je de thread_test () functie aangeroepen.
- Dit is de thread_test () - methode die de waarde van i als argument aanneemt , deze bij elke iteratie met 1 verlaagt en de rest van de code doorloopt totdat i 0 wordt. In elke iteratie wordt de naam van de momenteel uitgevoerde thread afgedrukt en slaapt wacht seconden (wat ook als argument wordt gebruikt).
- thread1 = threadtester (1, "eerste thread", 1)
Hier maken we een thread en geven we de drie parameters door die we in __init__ hebben gedeclareerd. 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 werd aangeroepen, is voltooid.
Zoals u al weet, hebben de threads die zich in hetzelfde proces bevinden toegang tot het geheugen en de gegevens van dat proces. Als gevolg hiervan kunnen er fouten binnensluipen als meer dan één thread probeert de gegevens tegelijkertijd te wijzigen of er toegang toe te krijgen.
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 race-omstandigheden
Voordat u leert over impasses en race-omstandigheden, is het handig om een paar basisdefinities te begrijpen met betrekking tot gelijktijdig programmeren:
- Kritieke sectie
Het is een codefragment dat toegang heeft tot gedeelde variabelen of deze wijzigt en dat moet worden uitgevoerd als een atomaire transactie.
- Ander onderwerp
Het is het proces dat een CPU volgt om de status van een thread op te slaan voordat hij van de ene taak naar de andere overschakelt, zodat deze later vanaf hetzelfde punt kan worden hervat.
Deadlocks
Deadlocks zijn het meest gevreesde probleem waarmee ontwikkelaars worden geconfronteerd bij het schrijven van gelijktijdige / multithreaded applicaties in Python. De beste manier om impasses te begrijpen, is door gebruik te maken van het klassieke computerwetenschappelijke voorbeeldprobleem dat bekend staat als het Dining Philosophers Problem.
De probleemstelling voor eetfilosofen is als volgt:
Op een ronde tafel zitten vijf filosofen met vijf borden spaghetti (een soort pasta) en vijf vorken, zoals weergegeven in het diagram.
Een filosoof moet op elk moment eten of nadenken.
Bovendien moet een filosoof de twee vorken naast hem pakken (dwz de linker en rechter vorken) voordat hij de spaghetti kan eten. Het probleem van een impasse doet zich voor wanneer alle vijf filosofen tegelijkertijd hun rechtervorken oppakken.
Aangezien elk van de filosofen één vork heeft, zullen ze allemaal wachten tot de anderen hun vork neerleggen. Als gevolg hiervan zal geen van hen spaghetti kunnen eten.
Evenzo treedt in een gelijktijdig systeem een impasse op wanneer verschillende threads of processen (filosofen) tegelijkertijd proberen de gedeelde systeembronnen (forks) te verwerven. Als gevolg hiervan krijgt geen van de processen de kans om uit te voeren terwijl ze wachten op een andere bron die in het bezit is van een ander proces.
Race voorwaarden
Een raceconditie is een ongewenste toestand van een programma die optreedt wanneer een systeem twee of meer bewerkingen tegelijkertijd uitvoert. Beschouw bijvoorbeeld deze eenvoudige for-lus:
i=0; # a global variablefor x in range(100):print(i)i+=1;
Als u n aantal threads maakt die deze code tegelijk uitvoeren, kunt u de waarde van i (die wordt gedeeld door de threads) niet bepalen wanneer het programma de uitvoering heeft voltooid. Dit komt omdat in een echte multithreading-omgeving de threads elkaar kunnen overlappen, en de waarde van i die werd opgehaald en gewijzigd door een thread kan tussendoor veranderen wanneer een andere thread er toegang toe krijgt.
Dit zijn de twee hoofdklassen 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.
Threads synchroniseren
Om het hoofd te bieden aan race-omstandigheden, impasses en andere thread-gebaseerde problemen, biedt de threading-module het Lock- object. Het idee is dat wanneer een thread toegang wil tot een specifieke bron, deze een vergrendeling krijgt voor die bron. Zodra een thread een bepaalde bron vergrendelt, kan geen enkele andere thread er toegang toe krijgen totdat de vergrendeling wordt vrijgegeven. Als gevolg hiervan zullen de wijzigingen aan de bron atomair zijn en zullen race-omstandigheden worden afgewend.
Een slot is een synchronisatieprimitief op laag niveau geïmplementeerd door de __thread- module. Een slot kan op elk moment in een van de 2 toestanden zijn: vergrendeld of ontgrendeld. Het ondersteunt twee methoden:
- verkrijgen()
Wanneer de vergrendelingsstatus is ontgrendeld, zal het aanroepen van de methode acquir () de status wijzigen in vergrendeld en terugkeren. Als de status echter is vergrendeld, wordt de aanroep van acquir () geblokkeerd totdat de methode release () wordt aangeroepen door een andere thread.
- vrijlating()
De methode release () wordt gebruikt om de status in te stellen op ontgrendeld, dwz om een vergrendeling op te heffen. Het kan worden aangeroepen door elke thread, niet noodzakelijk degene die het slot heeft verworven.
Hier is een voorbeeld van het gebruik van vergrendelingen in uw apps. Start uw IDLE en typ het volgende:
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()
Druk nu op F5. Je zou een output als deze moeten zien:
CODE UITLEG
- Hier maakt u eenvoudig een nieuw slot door de fabrieksfunctie threading.Lock () aan te roepen . Intern retourneert Lock () een instantie van de meest effectieve concrete Lock-klasse die wordt onderhouden door het platform.
- In de eerste instructie verkrijgt u het slot door de methode acquir () aan te roepen. Wanneer het slot is toegekend, print je "slot verworven" naar de console. Zodra alle code die u wilt dat de thread wordt uitgevoerd, is uitgevoerd, heft u de vergrendeling op door de methode release () aan te roepen.
De theorie is prima, maar hoe weet je dat het slot echt werkte? Als u naar de uitvoer kijkt, ziet u dat elk van de afdrukopdrachten exact één regel tegelijk wordt afgedrukt. Bedenk dat, in een eerder voorbeeld, de uitvoer van print lukraak was omdat meerdere threads tegelijkertijd toegang hadden tot de methode print (). Hier wordt de afdrukfunctie pas aangeroepen nadat de vergrendeling is verworven. De uitgangen worden dus een voor een en regel voor regel weergegeven.
Afgezien van vergrendelingen ondersteunt python ook enkele andere mechanismen om thread-synchronisatie af te handelen, zoals hieronder vermeld:
- RLocks
- Semaforen
- Voorwaarden
- Evenementen, en
- Belemmeringen
Global Interpreter Lock (en hoe ermee om te gaan)
Voordat we ingaan op de details van python's GIL, laten we een paar termen definiëren die nuttig zullen zijn om de komende sectie 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 toegang heeft tot het bestandssysteem via het besturingssysteem
- CPython: het is de referentie- implementatie van Python en kan worden omschreven als de tolk geschreven in C en Python (programmeertaal).
Wat is GIL in Python?
Global Interpreter Lock (GIL) in python is een procesvergrendeling of een mutex die wordt gebruikt tijdens het omgaan met de processen. Het zorgt ervoor dat één thread tegelijkertijd toegang heeft tot een bepaalde bron en het voorkomt ook het gebruik van objecten en bytecodes tegelijk. Dit komt de single-threaded programma's ten goede bij een prestatieverhoging. GIL in python is heel eenvoudig en gemakkelijk te implementeren.
Een vergrendeling kan worden gebruikt om ervoor te zorgen dat slechts één thread tegelijkertijd toegang heeft tot een bepaalde bron.
Een van de kenmerken van Python is dat het een globale vergrendeling gebruikt voor elk interpreterproces, wat betekent dat elk proces de Python-interpreter zelf als een hulpmiddel behandelt.
Stel dat je een Python-programma hebt geschreven dat twee threads gebruikt om zowel CPU- als 'I / O'-bewerkingen uit te voeren. Als u dit programma uitvoert, gebeurt dit als volgt:
- De python-interpreter maakt een nieuw proces en brengt de threads uit
- Wanneer thread-1 begint te lopen, verkrijgt deze eerst de GIL en vergrendelt deze.
- 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 dit moment zal het de GIL vrijgeven, en thread-2 zal het verkrijgen.
- Na het voltooien van de I / O-ops, als thread-1 nu wil uitvoeren, zal het opnieuw moeten wachten tot de GIL wordt vrijgegeven door thread-2.
Hierdoor heeft slechts één thread tegelijkertijd toegang tot de interpreter, wat betekent dat er op een bepaald moment slechts één thread python-code zal uitvoeren.
Dit is prima in een single-core processor omdat het tijdschijven zou gebruiken (zie het eerste deel van deze tutorial) om de threads af te handelen. In het geval van multi-coreprocessors zal een CPU-gebonden functie die wordt uitgevoerd op meerdere threads echter een aanzienlijke invloed hebben op de efficiëntie van het programma, aangezien het niet alle beschikbare cores tegelijkertijd gebruikt.
Waarom was GIL nodig?
De CPython garbage collector maakt gebruik van een efficiënte geheugenbeheertechniek die bekend staat als referentietelling. Zo werkt het: 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 tupels, lijsten, enz.). Evenzo wordt het aantal verwijzingen verlaagd wanneer de verwijzing buiten het bereik valt of wanneer de instructie del wordt aangeroepen. Wanneer de referentietelling van een object 0 bereikt, wordt het garbage verzameld en wordt het toegewezen geheugen vrijgemaakt.
Maar het probleem is dat de referentietellingvariabele net als elke andere globale variabele vatbaar is voor raceomstandigheden. Om dit probleem op te lossen, hebben de ontwikkelaars van python besloten om de globale interpreter lock te gebruiken. De andere optie was om een vergrendeling toe te voegen aan elk object, wat zou hebben geresulteerd in impasses en verhoogde overhead door acquisitie () en release () oproepen.
Daarom is GIL een belangrijke beperking voor multithreaded python-programma's die zware CPU-gebonden bewerkingen uitvoeren (waardoor ze in feite single-threaded worden). Als je gebruik wilt maken van meerdere CPU-cores in je applicatie, gebruik dan de multiprocessing- module.
Overzicht
- Python ondersteunt 2 modules voor multithreading:
- __thread- module: het biedt een implementatie op laag niveau voor threading en is verouderd.
- threading-module : het biedt een implementatie op hoog niveau voor multithreading en is de huidige standaard.
- Om een thread te maken met de inrijgmodule, moet u het volgende doen:
- Maak een klasse die de klasse Thread uitbreidt .
- Overschrijf zijn constructor (__init__).
- Overschrijf de run () - methode.
- Maak een object van deze klasse.
- Een thread kan worden uitgevoerd door de start () - methode aan te roepen .
- De methode join () kan worden gebruikt om andere threads te blokkeren totdat deze thread (de thread waarop join werd aangeroepen) de uitvoering heeft voltooid.
- Een racevoorwaarde doet zich voor wanneer meerdere threads tegelijkertijd toegang krijgen tot een gedeelde bron of deze wijzigen.
- Het kan worden vermeden door threads te synchroniseren.
- Python ondersteunt 6 manieren om threads te synchroniseren:
- Sloten
- RLocks
- Semaforen
- Voorwaarden
- Evenementen, en
- Belemmeringen
- Vergrendelingen laten alleen een bepaalde draad toe die de vergrendeling heeft gekregen om de kritieke sectie te betreden.
- Een slot heeft 2 primaire methoden:
- verwerven () : Het zet de vergrendelingsstatus op vergrendeld. Als een vergrendeld object wordt aangeroepen, wordt het geblokkeerd totdat de bron vrij is.
- release () : Het stelt de vergrendelingsstatus in op ontgrendeld en keert terug. Als het wordt aangeroepen voor een ontgrendeld object, retourneert het false.
- De globale tolkvergrendeling is een mechanisme waarmee slechts één CPython-tolkproces tegelijk kan worden uitgevoerd.
- Het werd gebruikt om de referentietelfunctie van de garbagecollector van CPythons te vergemakkelijken.
- Om Python-apps te maken met zware CPU-gebonden bewerkingen, moet u de multiprocessing-module gebruiken.