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.

  • 🔘 Tråd kontra process: En tråd är en exekveringsenhet inuti en process, och trådar delar processdata medan de körs individuellt.
  • ☑️ Två moduler: Den föråldrade modulen _thread erbjuder anrop på låg nivå, medan modulen för trådning på högre nivå är den moderna standarden för att bygga trådar.
  • Skapa trådar: Utöka Thread-klassen, åsidosätt __init__ och run(), anropa sedan start() för att starta och join() för att vänta på att det ska vara klart.
  • 🧪 Undvik tävlingsförhållanden: Ett lås tillåter endast en tråd att komma in i ett kritiskt avsnitt åt gången, vilket förhindrar dödlägen och skadade delade data.
  • 🛠️ Förstå GIL: Det globala tolklåset tillåter att en tråd körs Python bytekod åt gången, så CPU-bundet arbete behöver multiprocessering.
  • 🤖 AI-arbetsbelastningar: Multitrådning snabbar upp input/output-bundna AI-steg som datainläsning, medan NumPy och PyTorch släpper GIL för parallell beräkning.

Multitråda in Python

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:

  1. Ocuco-landskapet gänga modul och
  2. 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:

Trådmodulen

Du kommer att lära dig mer om tävlingsförhållanden och hur du hanterar dem i kommande avsnitt.

Trådmodulen

KOD FÖRKLARING

  1. Dessa satser importerar time- och thread-modulerna, vilka används för att hantera exekveringen och fördröjningen av Python trådar.
  2. 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.
  3. 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

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:

  1. definiera en klass som utökar Thread-klassen
  2. Åsidosätt __i det__ konstruktör
  3. Å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:

Bakgrund: Trådklassen

KOD FÖRKLARING

Bakgrund: Trådklassen

  1. 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.
  2. 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().
  3. 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).
  4. 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.
  5. thread2.start() Startmetoden används för att starta exekveringen av en tråd. Internt anropar start()-funktionen run()-metoden för din klass.
  6. 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

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:

  1. 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.
  2. 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:

Synchronizing trådar

KOD FÖRKLARING

Synchronizing trådar

  1. 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.
  2. 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:

  1. Lås
  2. Semaphores
  3. Villkor
  4. Händelser och
  5. 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:

  1. CPU-bunden kod: detta hänvisar till all kod som kommer att exekveras direkt av processorn.
  2. I/O-bunden kod: detta kan vara vilken kod som helst som åtkommer filsystemet via operativsystemet.
  3. 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:

  1. Ocuco-landskapet Python interpreter skapar en ny process och skapar trådarna.
  2. När tråd-1 börjar köra kommer den först att förvärva GIL och låsa den.
  3. 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.
  4. 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.
  5. 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.

Vanliga frågor

Använd trådning för I/O-bundet arbete, såsom nätverksanrop, filåtkomst eller databasfrågor, där trådar väntar på externa resurser. Använd multiprocessering för CPU-bundet arbete, såsom tunga beräkningar, eftersom det körs på flera kärnor och kringgår GIL.

Inte som standard. PEP 703 introducerade en valfri fritrådad inbyggnad Python 3.13 som inaktiverar GIL, och Python 3.14 fortsätter med det. Standardversioner levererar fortfarande GIL, så de flesta program fortsätter att köras exakt som tidigare.

En daemontråd körs i bakgrunden och blockerar inte programmet från att avslutas. När endast daemontrådar återstår, Python stänger av dem och avslutar. Sätt en med thread.daemon = True innan start() anropas.

Klassen Thread returnerar inte run()-resultatet direkt. Lagra värdet på thread-objektet eller en delad kö, eller använd concurrent.futures.ThreadPoolExecutor, vars submit()-metod returnerar en Future som du läser med result().

ThreadPoolExecutor är en hjälpfunktion på hög nivå som hanterar en pool av arbetstrådar åt dig. Du skickar in uppgifter med submit() eller map(), och den hanterar skapande, återanvändning och rensning av trådar, och returnerar Future-objekt som innehåller varje resultat.

Du kan skapa hundratals eller tusentals trådar, men GIL tillåter bara en exekvering Python bytekod när som helst. De överlappar fortfarande varandra under I/O-väntningar, så många samtidiga trådar hjälper I/O-bundna program mycket mer än CPU-bundna.

Ja, främst för I/O-bundna steg som att ladda data, anropa API:er eller läsa filer. Tung träning är CPU-bunden, så bibliotek som NumPy och PyTorch släppa GIL i C-kod eller förlita dig på multiprocessering för sann parallellism.

Ja. GitHub Copilot kompletterar automatiskt vanliga mönster som trådunderklasser, låsanvändning och ThreadPoolExecutor-inställningar från en kommentar eller ett funktionsnamn. Granska alltid den genererade koden för kapplöpningsvillkor och korrekt låshantering innan du förlitar dig på den.

Sammanfatta detta inlägg med: