Multitråda in Python med Exempel: Lär dig GIL i Python
⚡ Smart sammanfattning
Multitråda in Python kör flera trådar inuti en process så att de delar minne och arbetar samtidigt. Trådningsmodulen skapar och hanterar dessa trådar, medan Global Interpreter Lock begränsar verklig parallellitet, vilket gör tekniken bäst lämpad för input/output-bundna uppgifter.

Ocuco-landskapet Python programmeringsspråk låter dig använda multiprocessering eller multitrådning. I den här handledningen lär du dig hur man skriver multitrådade applikationer i Python.
Vad är en tråd?
En tråd är en exekveringsenhet i samtidig programmering. Multitrådning är en teknik som gör det möjligt för en processor att utföra många uppgifter i en process samtidigt. Dessa trådar kan exekveras individuellt samtidigt som de delar sina processresurser.
Vad är en process?
En process är i grunden 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 enkel och effektiv. Trådar är lättare än processer. Flera trådar kan köras individuellt samtidigt som de delar sina processresurser. Syftet med multitrådning är att köra flera uppgifter och funktioner 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, tänk dig följande scenario: En .exe-fil på din dator är ett program. När du öppnar den laddar operativsystemet den till minnet och processorn kör den. Den instans av programmet som nu körs kallas processen.
Varje process har två grundläggande komponenter:
- Ocuco-landskapet Code
- Uppgifterna
Nu kan en process innehålla en eller flera anropade underdelar trådar. Detta beror på operativsystemets arkitektur. Man 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 parallellitet.
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 multitrådning
Python stöder konstruktioner för både multiprocessering och multitrådning. I den här handledningen kommer du främst att fokusera på att implementera flertrådig applikationer med PythonDet finns två huvudmoduler som kan användas för att hantera trådar i Python:
- Ocuco-landskapet gänga modul och
- Ocuco-landskapet gängning modul
Men i Pythonfinns det också något som kallas global interpreter lock (GIL). Det möjliggör inte mycket prestandavinst och kan 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 avser att driftsätta. Trådmodulen har endast tagits upp 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 kommande avsnitt.
KOD FÖRKLARING
- Dessa satser importerar time- och thread-modulerna, vilka 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 anropade den. När iterationen är klar skriver den ut ett meddelande som säger att tråden har körts klart.
- Detta är huvuddelen av ditt program. Här ringer du helt enkelt start_new_thread metoden med thread_test funktion som ett argument. Detta skapar en ny tråd för funktionen du skickar som ett argument och börjar köra den. Observera att du kan ersätta detta (thread_test) med vilken annan funktion som helst som du vill köra som en tråd.
Träningsmodulen
Den här modulen är en övergripande implementering av trådning i Python och de facto standarden för att hantera flertrådade applikationer. Den erbjuder ett brett utbud av funktioner jämfört med trådmodulen.
Struktur för gängningsmodulen
Här är en lista över några användbara funktioner som definieras i denna modul:
| Funktionsnamn | BESKRIVNING |
|---|---|
| activeCount() | Returnerar räkningen av Tråd föremål som fortfarande lever. |
| 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ådmodulen ä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 Python applikationen är att deklarera en klass som utökar Thread-klassen 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 Thread-klassen
- Åsidosätt __i det__ konstruktör
- Åsidosätt springa() metod
När ett trådobjekt har gjorts, Start() metoden kan användas för att påbörja utförandet av denna aktivitet, och Ansluta sig() metoden kan användas för att blockera all annan kod tills den aktuella aktiviteten avslutas.
Nu ska vi försöka använda threading-modulen för att implementera ditt tidigare exempel. Återigen, starta 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 i vårt tidigare exempel. Här importerar du tids- och trådmodulen, som används för att hantera exekveringen och fördröjningarna av Python trådar.
- I den här biten skapar du en klass som kallas threadtester, som ärver eller utökar Tråd klassen för trådmodulen. Detta är ett av de vanligaste sätten att skapa trådar i PythonDu 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 loopar igenom resten av koden tills i blir 0. I varje iteration skriver den ut namnet på den tråd som för närvarande körs och sover i väntesekunder (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ådar som är i samma process tillgång till minnet och data i 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 ser du de olika typer av komplikationer som kan uppstå när trådar kommer åt data och den kritiska sektionen utan att kontrollera befintliga åtkomsttransaktioner.
Dödläge och tävlingsförhållanden
Innan man lär sig om dödlägen och kapplöpningsförhållanden är det bra att förstå några grundläggande definitioner relaterade till samtidig programmering:
- Kritisk sektion: Det är ett kodfragment som åtkommer eller modifierar delade variabler och måste utföras som en atomär transaktion.
- Kontextväxling: Det är den process som en processor följer för att lagra tillståndet för en tråd innan den byter 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 PythonDet 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 vid ett runt bord med fem tallrikar spaghetti (en sorts pasta) och fem gafflar, som visas i diagrammet.
Matplats Philosophers problem
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 kappvillkor ä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 dig till exempel denna enkla for-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 samtidigt, kan du inte bestämma värdet på i (som delas av trådarna) när programmet är klart. Detta beror på att trådarna i en verklig multitrådningsmiljö kan överlappa varandra, och värdet på i som hämtades och modifierades av en tråd kan ändras däremellan när någon annan tråd öppnar den.
Dessa är de två huvudklasserna av problem som kan uppstå i en flertrådad eller distribuerad Python applikation. I nästa avsnitt lär du 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. Ett lås kan när som helst befinna sig i ett av två tillstånd: låst or olåst. Den stöder två metoder:
- förvärva(): När låst tillstånd är upplåst, kommer anrop av acquire()-metoden att ändra tillståndet till locked och returnera. Om tillståndet däremot är locked blockeras anropet av acquire() tills release()-metoden anropas av någon annan tråd.
- släppa(): 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. Starta 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å utdata ser du att var och en av print-satserna skriver ut exakt en rad i taget. Kom ihåg att i ett tidigare exempel var utdata från print slumpmässiga eftersom flera trådar använde print()-metoden samtidigt. Här anropas print-funktionen först efter att låset har hämtats. Så utdata visas en i taget och rad för rad.
Förutom lås, Python stöder även några andra mekanismer för att hantera trådsynkronisering, enligt nedan:
- Lås
- Semaphores
- Villkor
- Händelser och
- Hinder
Globalt tolklås (och hur man hanterar det)
Innan vi går in på detaljerna om 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 all kod som kommer att exekveras direkt av processorn.
- I/O-bunden kod: detta kan vara vilken kod som helst som åtkommer 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) in Python är ett processlås eller en mutex som används vid hantering av processer. Det säkerställer att en tråd kan komma åt en viss resurs åt gången, och det förhindrar också användning av objekt och bytekoder samtidigt. Detta gynnar de enkeltrådade programmen med en prestandaökning. GIL i Python är mycket enkel 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 tolkningsprocess, vilket innebär att varje process behandlar Python tolka sig själv som en resurs.
Anta till exempel att du har skrivit en Python ett program som använder två trådar för att utföra både CPU- och 'I/O'-operationer. När du kör programmet händer följande:
- Ocuco-landskapet Python interpreter 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 åtkomst till tolken åt gången, vilket innebär att det bara kommer att finnas en tråd som körs Python kod vid en given tidpunkt.
Detta är okej i en enkärnig processor eftersom den skulle använda tidsdelning (se det första avsnittet i den här handledningen) för att hantera trådarna. Men när det gäller 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 inte faktiskt kommer att använda alla tillgängliga kärnor samtidigt.
Varför behövdes GIL?
CPython sophämtaren använder en effektiv minneshanteringsteknik som kallas referensräkning. Så här fungerar det: Varje objekt i Python har ett referensantal, vilket ökas när det tilldelas ett nytt variabelnamn eller läggs till i en container (som tupler, listor, etc.). Likaså minskas referensantalet när referensen hamnar utanför räckvidden eller när del-satsen anropas. När referensantalet för ett objekt når 0, sophämtas det och det tilldelade minnet frigörs.
Men problemet är att referensantalvariabeln är benägen att utsättas för kapplöpningsförhållanden precis som alla andra globala variabler. För att lösa detta problem har utvecklarna av Python bestämde sig för att använda det globala tolklåset. Det andra alternativet var att lägga till ett lås till varje objekt, vilket skulle ha resulterat i dödlägen och ökad omkostnad från acquire()- och release()-anrop.
Därför är GIL en betydande begränsning för flertrådade Python program som kör tunga CPU-bundna operationer (vilket i praktiken gör dem enkeltrådade). Om du vill använda flera CPU-kärnor i din applikation, använd multibehandlings modul istället.








