Multitråda in Python med Exempel: Lär dig GIL i Python
Vad är en tråd?
En tråd är en enhet för exekvering vid samtidig programmering. Multithreading är en teknik som gör att en CPU kan utföra många uppgifter i en process samtidigt. Dessa trådar kan köras individuellt samtidigt som de delar sina processresurser.
Vad är en process?
En process är i princip det program som körs. När du startar ett program på din dator (som en webbläsare eller textredigerare) skapar operativsystemet en processen.
Vad är Multithreading i Python?
Multitråda in Python programmering är en välkänd teknik där flera trådar i en process delar sitt datautrymme med huvudtråden vilket gör informationsdelning och kommunikation inom trådar enkelt och effektivt. Trådar är lättare än processer. Flera trådar kan köras individuellt samtidigt som de delar sina processresurser. Syftet med multithreading är att köra flera uppgifter och funktionsceller samtidigt.
Vad är Multiprocessing?
Multi låter dig köra flera orelaterade processer samtidigt. Dessa processer delar inte sina resurser och kommunicerar via IPC.
Python Multithreading vs Multiprocessing
För att förstå processer och trådar, överväg detta scenario: En .exe-fil på din dator är ett program. När du öppnar det laddar operativsystemet det i minnet och processorn kör det. Den instans av programmet som nu körs kallas processen.
Varje process kommer att ha två grundläggande komponenter:
- Koden
- Uppgifterna
Nu kan en process innehålla en eller flera anropade underdelar trådar. Detta beror på OS-arkitekturen. Du kan tänka på en tråd som en del av processen som kan köras separat av operativsystemet.
Med andra ord är det en ström av instruktioner som kan köras oberoende av operativsystemet. Trådar inom en enda process delar data från den processen och är utformade för att fungera tillsammans för att underlätta parallellism.
Varför använda Multithreading?
Multithreading låter dig dela upp en applikation i flera deluppgifter och köra dessa uppgifter samtidigt. Om du använder multithreading på rätt sätt kan din applikationshastighet, prestanda och rendering förbättras.
Python MultiThreading
Python stöder konstruktioner för både multiprocessing och multithreading. I den här handledningen kommer du i första hand att fokusera på implementering flertrådig applikationer med python. Det finns två huvudmoduler som kan användas för att hantera trådar i Python:
- Din gänga modul och
- Din gängning modul
Men i python finns det också något som kallas ett globalt tolklås (GIL). Det tillåter inte mycket prestandavinst och kanske till och med minska prestandan för vissa flertrådade applikationer. Du kommer att lära dig allt om det i de kommande avsnitten av denna handledning.
Modulerna Tråd och Träning
De två modulerna som du kommer att lära dig om i den här handledningen är trådmodul och gängningsmodul.
Trådmodulen har dock länge varit utfasad. Börjar med Python 3, har den betecknats som föråldrad och är endast tillgänglig som __tråd för bakåtkompatibilitet.
Du bör använda den högre nivån gängning modul för applikationer som du tänker distribuera. Trådmodulen har endast behandlats här i utbildningssyfte.
Trådmodulen
Syntaxen för att skapa en ny tråd med den här modulen är följande:
thread.start_new_thread(function_name, arguments)
Okej, nu har du täckt den grundläggande teorin för att börja koda. Så öppna din IDLE eller ett anteckningsblock och skriv in följande:
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))
Spara filen och tryck på F5 för att köra programmet. Om allt gjordes på rätt sätt är det här utgången som du bör se:
Du kommer att lära dig mer om tävlingsförhållanden och hur du hanterar dem i de kommande avsnitten
KOD FÖRKLARING
- Dessa uttalanden importerar tids- och trådmodulen som används för att hantera exekveringen och fördröjningen av Python trådar.
- Här har du definierat en funktion som kallas thread_test, som kommer att kallas av start_new_thread metod. Funktionen kör en while-loop i fyra iterationer och skriver ut namnet på tråden som kallade den. När iterationen är klar skrivs ett meddelande ut som säger att tråden har körts färdigt.
- Detta är huvuddelen av ditt program. Här ringer du helt enkelt start_new_thread metoden med thread_test fungerar som ett argument. Detta skapar en ny tråd för funktionen du skickar som argument och börjar köra den. Observera att du kan ersätta denna (tråd_test) med någon annan funktion som du vill köra som en tråd.
Träningsmodulen
Den här modulen är implementeringen på hög nivå av trådning i python och de facto-standarden för att hantera flertrådade applikationer. Den ger ett brett utbud av funktioner jämfört med gängmodulen.
Här är en lista över några användbara funktioner som definieras i denna modul:
Funktionsnamn | Description |
---|---|
activeCount() | Returnerar räkningen av Tråd föremål som fortfarande är vid liv |
aktuell tråd() | Returnerar det aktuella objektet i klassen Thread. |
räkna upp() | Listar alla aktiva trådobjekt. |
isDaemon() | Returnerar sant om tråden är en demon. |
lever() | Returnerar sant om tråden fortfarande lever. |
Trådklassmetoder | |
Start() | Startar aktiviteten för en tråd. Den måste bara anropas en gång för varje tråd eftersom den kommer att ge ett körtidsfel om den anropas flera gånger. |
springa() | Denna metod anger aktiviteten för en tråd och kan åsidosättas av en klass som utökar klassen Thread. |
Ansluta sig() | Det blockerar exekveringen av annan kod tills tråden som join()-metoden anropades på avslutas. |
Bakgrund: Trådklassen
Innan du börjar koda flertrådade program med hjälp av trådningsmodulen är det viktigt att förstå trådklassen. Trådklassen är den primära klassen som definierar mallen och operationerna för en tråd i python.
Det vanligaste sättet att skapa en flertrådad pythonapplikation är att deklarera en klass som utökar klassen Thread och åsidosätter dess run()-metod.
Klassen Thread betyder sammanfattningsvis en kodsekvens som körs i en separat gänga av kontroll.
Så när du skriver en flertrådad app kommer du att göra följande:
- definiera en klass som utökar klassen Thread
- Åsidosätt __i det__ konstruktör
- Åsidosätt springa() metod
När ett trådobjekt har gjorts, Start() metod kan användas för att börja utföra denna aktivitet och Ansluta sig() metoden kan användas för att blockera all annan kod tills den aktuella aktiviteten avslutas.
Låt oss nu försöka använda trådningsmodulen för att implementera ditt tidigare exempel. Återigen, elda upp din IDLE och skriv in följande:
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()
Detta kommer att vara utdata när du kör ovanstående kod:
KOD FÖRKLARING
- Den här delen är densamma som vårt tidigare exempel. Här importerar du tids- och trådmodulen som används för att hantera exekvering och förseningar av Python trådar.
- I den här biten skapar du en klass som kallas threadtester, som ärver eller utökar Tråd klass för gängningsmodulen. Detta är ett av de vanligaste sätten att skapa trådar i python. Du bör dock bara åsidosätta konstruktorn och springa() metod i din app. Som du kan se i ovanstående kodexempel, är __i det__ metod (konstruktör) har åsidosatts. På samma sätt har du också åsidosatt springa() metod. Den innehåller koden som du vill köra i en tråd. I det här exemplet har du anropat funktionen thread_test().
- Detta är metoden thread_test() som tar värdet av i som ett argument, minskar det med 1 vid varje iteration och går igenom resten av koden tills i blir 0. I varje iteration skriver den ut namnet på den körande tråden och sover i väntan sekunder (vilket också tas som ett argument ).
- thread1 = threadtester(1, "First Thread", 1) Här skapar vi en tråd och skickar de tre parametrarna som vi deklarerade i __init__. Den första parametern är id för tråden, den andra parametern är trådens namn och den tredje parametern är räknaren, som bestämmer hur många gånger while-slingan ska köras.
- thread2.start()Startmetoden används för att starta exekveringen av en tråd. Internt anropar start()-funktionen run()-metoden för din klass.
- thread3.join() Metoden join() blockerar exekveringen av annan kod och väntar tills tråden som den anropades på slutar.
Som du redan vet har trådarna som är i samma process tillgång till minnet och data för den processen. Som ett resultat, om mer än en tråd försöker ändra eller komma åt data samtidigt, kan fel smyga sig in.
I nästa avsnitt kommer du att se de olika typerna av komplikationer som kan dyka upp när trådar får tillgång till data och kritiska avsnitt utan att leta efter befintliga åtkomsttransaktioner.
Dödläge och tävlingsförhållanden
Innan du lär dig om dödlägen och tävlingsförhållanden, är det bra att förstå några grundläggande definitioner relaterade till samtidig programmering:
- Critical SectionDet är ett fragment av kod som får åtkomst till eller modifierar delade variabler och måste utföras som en atomär transaktion.
- Context SwitchDet är den process som en CPU följer för att lagra tillståndet för en tråd innan den ändras från en uppgift till en annan så att den kan återupptas från samma punkt senare.
Blockeringar
Blockeringar är det mest fruktade problemet som utvecklare möter när de skriver samtidiga/flertrådade applikationer i python. Det bästa sättet att förstå dödlägen är att använda det klassiska datavetenskapliga exempelproblemet som kallas Matplats Philosophers problem.
Problemformuleringen för middagsfilosofer är följande:
Fem filosofer sitter på ett runt bord med fem tallrikar spagetti (en typ av pasta) och fem gafflar, som visas i diagrammet.
Vid varje given tidpunkt måste en filosof antingen äta eller tänka.
Dessutom måste en filosof ta de två gafflarna intill honom (dvs. vänster och höger gafflar) innan han kan äta spaghettin. Problemet med dödläge uppstår när alla fem filosoferna tar upp sina högra gafflar samtidigt.
Eftersom var och en av filosoferna har en gaffel, kommer de alla att vänta på att de andra ska lägga ner gaffeln. Som ett resultat kommer ingen av dem att kunna äta spagetti.
På liknande sätt, i ett samtidigt system, uppstår ett dödläge när olika trådar eller processer (filosofer) försöker skaffa de delade systemresurserna (gafflar) samtidigt. Som ett resultat får ingen av processerna en chans att köra eftersom de väntar på en annan resurs som innehas av någon annan process.
Race villkor
Ett race-tillstånd är ett oönskat tillstånd i ett program som uppstår när ett system utför två eller flera operationer samtidigt. Tänk till exempel detta enkla för loop:
i=0; # a global variable for x in range(100): print(i) i+=1;
Om du skapar n antal trådar som kör den här koden på en gång kan du inte bestämma värdet på i (som delas av trådarna) när programmet slutar köras. Detta beror på att i en verklig multithreading-miljö kan trådarna överlappa varandra, och värdet på i som hämtades och modifierades av en tråd kan ändras mellan när någon annan tråd kommer åt den.
Dessa är de två huvudklasserna av problem som kan uppstå i en flertrådad eller distribuerad pythonapplikation. I nästa avsnitt kommer du att lära dig hur du löser detta problem genom att synkronisera trådar.
Synchroniserande trådar
För att hantera tävlingsförhållanden, dödläge och andra trådbaserade problem tillhandahåller gängningsmodulen Lås objekt. Tanken är att när en tråd vill ha tillgång till en specifik resurs skaffar den ett lås för den resursen. När en tråd låser en viss resurs kan ingen annan tråd komma åt den förrän låset släpps. Som ett resultat kommer förändringarna av resursen att vara atomära och rasförhållandena kommer att avvärjas.
Ett lås är en lågnivåsynkroniseringsprimitiv implementerad av __tråd modul. Vid varje given tidpunkt kan ett lås vara i ett av två tillstånd: låst or olåst. Den stöder två metoder:
- tillägna sig()När låstillståndet är upplåst, kommer anrop av metoden förvärv() att ändra tillståndet till låst och återgå. Men om tillståndet är låst, blockeras anropet att förvärva() tills release()-metoden anropas av någon annan tråd.
- släpp()Metoden release() används för att ställa in tillståndet till olåst, dvs för att frigöra ett lås. Det kan anropas av vilken tråd som helst, inte nödvändigtvis den som skaffade låset.
Här är ett exempel på hur du använder lås i dina appar. Fyra upp din IDLE och skriv följande:
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()
Tryck nu på F5. Du bör se en utgång så här:
KOD FÖRKLARING
- Här skapar du helt enkelt ett nytt lås genom att anropa threading.Lock() fabriksfunktion. Internt returnerar Lock() en instans av den mest effektiva konkreta Lock-klassen som underhålls av plattformen.
- I den första satsen förvärvar du låset genom att anropa förvärv()-metoden. När låset har beviljats skriver du ut "lås förvärvat" till konsolen. När all kod som du vill att tråden ska köra har körts släpper du låset genom att anropa release()-metoden.
Teorin är bra, men hur vet man att låset verkligen fungerade? Om du tittar på resultatet kommer du att se att var och en av utskriftssatserna skrivs ut exakt en rad åt gången. Kom ihåg att i ett tidigare exempel var utdata från print slumpmässiga eftersom flera trådar åtkomst till print()-metoden samtidigt. Här anropas utskriftsfunktionen först efter att låset har förvärvats. Så utgångarna visas en i taget och rad för rad.
Bortsett från lås, stöder python också några andra mekanismer för att hantera trådsynkronisering enligt listan nedan:
- Lås
- Semaphores
- Villkor
- Händelser och
- Hinder
Globalt tolklås (och hur man hanterar det)
Innan vi går in på detaljerna i pythons GIL, låt oss definiera några termer som kommer att vara användbara för att förstå det kommande avsnittet:
- CPU-bunden kod: detta hänvisar till vilken kod som helst som kommer att exekveras direkt av CPU:n.
- I/O-bunden kod: detta kan vara vilken kod som helst som kommer åt filsystemet via operativsystemet
- CPython: det är referensen genomförande of Python och kan beskrivas som tolken skriven i C och Python (programmeringsspråk).
Vad är GIL i Python?
Global Interpreter Lock (GIL) i python är ett processlås eller en mutex som används när man hanterar processerna. Det ser till att en tråd kan komma åt en viss resurs åt gången och det förhindrar också användningen av objekt och bytekoder på en gång. Detta gynnar de enkeltrådade programmen i en prestandaökning. GIL i python är väldigt enkelt och lätt att implementera.
Ett lås kan användas för att säkerställa att endast en tråd har tillgång till en viss resurs vid en given tidpunkt.
En av funktionerna i Python är att den använder ett globalt lås på varje tolkprocess, vilket innebär att varje process behandlar själva pythontolken som en resurs.
Anta till exempel att du har skrivit ett pythonprogram som använder två trådar för att utföra både CPU- och 'I/O'-operationer. När du kör det här programmet händer det här:
- Pythontolken skapar en ny process och skapar trådarna
- När tråd-1 börjar köra kommer den först att förvärva GIL och låsa den.
- Om tråd-2 vill köra nu måste den vänta på att GIL ska släppas även om en annan processor är ledig.
- Anta nu att tråd-1 väntar på en I/O-operation. För närvarande kommer den att släppa GIL, och tråd-2 kommer att förvärva den.
- Efter att ha slutfört I/O-operationerna, om tråd-1 vill köras nu, måste den återigen vänta på att GIL ska släppas av tråd-2.
På grund av detta kan endast en tråd komma åt tolken när som helst, vilket betyder att det bara kommer att finnas en tråd som exekverar pythonkod vid en given tidpunkt.
Det här är okej i en enkärnig processor eftersom den skulle använda tidsdelning (se första avsnittet i denna handledning) för att hantera trådarna. Men i fallet med flerkärniga processorer kommer en CPU-bunden funktion som körs på flera trådar att ha en avsevärd inverkan på programmets effektivitet eftersom det faktiskt inte kommer att använda alla tillgängliga kärnor samtidigt.
Varför behövdes GIL?
CPython garbage collector använder en effektiv minneshanteringsteknik som kallas referensräkning. Så här fungerar det: Varje objekt i python har ett referensantal, som ökas när det tilldelas ett nytt variabelnamn eller läggs till i en behållare (som tupler, listor, etc.). På samma sätt minskas referensantalet när referensen går utanför räckvidden eller när delsatsen anropas. När referenstalet för ett objekt når 0, samlas det upp skräp och det tilldelade minnet frigörs.
Men problemet är att referensräkningsvariabeln är benägen till rasförhållanden som vilken annan global variabel som helst. För att lösa detta problem beslutade utvecklarna av python att använda det globala tolklåset. Det andra alternativet var att lägga till ett lås till varje objekt som skulle ha resulterat i dödlägen och ökad overhead från anrop av förvärv() och release().
Därför är GIL en betydande begränsning för flertrådade pythonprogram som kör tunga CPU-bundna operationer (som effektivt gör dem entrådiga). Om du vill använda flera CPU-kärnor i din applikation, använd multibehandlings modul istället.
Sammanfattning
- Python stöder 2 moduler för multithreading:
- __tråd modul: Den ger en implementering på låg nivå för gängning och är föråldrad.
- gängningsmodul: Det ger en implementering på hög nivå för multithreading och är den nuvarande standarden.
- För att skapa en tråd med hjälp av trådningsmodulen måste du göra följande:
- Skapa en klass som utökar Tråd klass.
- Åsidosätt dess konstruktor (__init__).
- Åsidosätt dess springa() metod.
- Skapa ett objekt av denna klass.
- En tråd kan köras genom att anropa Start() metod.
- Din Ansluta sig() metod kan användas för att blockera andra trådar tills den här tråden (den som sammanfogningen anropades på) avslutar exekveringen.
- Ett race-tillstånd uppstår när flera trådar får åtkomst till eller modifierar en delad resurs samtidigt.
- Det kan undvikas genom Synchroniserande trådar.
- Python stöder 6 sätt att synkronisera trådar:
- Lås
- Lås
- Semaphores
- Villkor
- Händelser och
- Hinder
- Lås tillåter endast en viss tråd som har fått låset att komma in i den kritiska delen.
- Ett lås har två primära metoder:
- tillägna sig(): Den ställer in låsläget till låst. Om ett låst objekt anropas, blockeras det tills resursen är ledig.
- släpp(): Den ställer in låsläget till olåst och återvänder. Om ett olåst objekt anropas, returnerar det falskt.
- Det globala tolklåset är en mekanism genom vilken endast 1 CPython tolkprocessen kan köras åt gången.
- Den användes för att underlätta referensräkningsfunktionen för CPythons:s sophämtare.
- Att göra Python appar med tunga CPU-bundna operationer, bör du använda multiprocessormodulen.