Multithreading inn Python med Eksempel: Lær GIL i Python

⚡ Smart oppsummering

Multithreading inn Python kjører flere tråder i én prosess, slik at de deler minne og jobber samtidig. Trådmodulen oppretter og administrerer disse trådene, mens Global Interpreter Lock begrenser ekte parallellisme, noe som gjør teknikken best egnet for input/output-bundne oppgaver.

  • 🔘 Tråd kontra prosess: En tråd er en utførelsesenhet i en prosess, og tråder deler prosessdataene mens de kjører individuelt.
  • ☑️ To moduler: Den foreldede _thread-modulen tilbyr lavnivåkall, mens trådmodulen på høyere nivå er den moderne standarden for å bygge tråder.
  • Opprett tråder: Utvid Thread-klassen, overstyr __init__ og run(), og kall deretter start() for å starte og join() for å vente på fullføring.
  • 🧪 Unngå løpsforhold: En lås lar bare én tråd gå inn i en kritisk seksjon om gangen, noe som forhindrer vranglåser og ødelagte delte data.
  • 🛠️ Forstå GIL: Den globale tolkelåsen tillater at én tråd kjører Python bytekode om gangen, så CPU-bundet arbeid trenger multiprosessering.
  • 🤖 AI-arbeidsbelastninger: Flertråding fremskynder input/output-bundne AI-trinn som datainnlasting, mens NumPy og PyTorch frigjør GIL for parallell beregning.

Multithreading inn Python

Ocuco Python programmeringsspråk lar deg bruke flerprosessering eller flertråding. I denne veiledningen lærer du hvordan du skriver flertrådede applikasjoner i Python.

Hva er en tråd?

En tråd er en utførelsesenhet i samtidig programmering. Multitråding er en teknikk som lar en CPU utføre mange oppgaver i én prosess samtidig. Disse trådene kan kjøres individuelt mens de deler prosessressursene sine.

Hva er en prosess?

En prosess er i bunn og grunn programmet som kjører. Når du starter et program på datamaskinen din (som en nettleser eller et tekstredigeringsprogram), oppretter operativsystemet en prosess.

Hva er Multithreading i Python?

Multithreading inn Python Programmering er en velkjent teknikk der flere tråder i en prosess deler dataområdet sitt med hovedtråden, noe som gjør informasjonsdeling og kommunikasjon i tråder enkelt og effektivt. Tråder er lettere enn prosesser. Flere tråder kan kjøres individuelt mens de deler prosessressursene sine. Formålet med flertråding er å kjøre flere oppgaver og funksjoner samtidig.

Hva er multiprosessering?

multi lar deg kjøre flere urelaterte prosesser samtidig. Disse prosessene deler ikke ressursene sine og kommuniserer gjennom IPC.

Python Multithreading vs Multiprocessing

For å forstå prosesser og tråder, bør du vurdere dette scenarioet: En .exe-fil på datamaskinen din er et program. Når du åpner den, laster operativsystemet den inn i minnet, og CPU-en kjører den. Forekomsten av programmet som nå kjører kalles prosessen.

Enhver prosess har to grunnleggende komponenter:

  • Ocuco Code
  • Dataen

Nå kan en prosess inneholde en eller flere underdeler kalt tråder. Dette avhenger av operativsystemets arkitektur. Du kan tenke på en tråd som en del av prosessen som kan kjøres separat av operativsystemet.

Med andre ord er det en strøm av instruksjoner som kan kjøres uavhengig av operativsystemet. Tråder innenfor en enkelt prosess deler dataene fra den prosessen og er designet for å fungere sammen for å legge til rette for parallellitet.

Hvorfor bruke Multithreading?

Multithreading lar deg bryte ned en applikasjon i flere underoppgaver og kjøre disse oppgavene samtidig. Hvis du bruker multithreading riktig, kan applikasjonens hastighet, ytelse og gjengivelse forbedres.

Python multithreading

Python støtter konstruksjoner for både flerprosessering og flertråding. I denne veiledningen vil du primært fokusere på implementering flertråds applikasjoner med PythonDet finnes to hovedmoduler som kan brukes til å håndtere tråder i Python:

  1. Ocuco tråden modul, og
  2. Ocuco træ moduler

Imidlertid i Python, finnes det også noe som kalles en global tolkelås (GIL). Den gir ikke mye ytelsesøkning og kan til og med redusere ytelsen til noen flertrådede applikasjoner. Du vil lære alt om det i de kommende delene av denne opplæringen.

Modulene Tråd og Tråding

De to modulene du vil lære om i denne opplæringen er trådmodul og gjengemodul.

Trådmodulen har imidlertid lenge vært utdatert. Begynner med Python 3, er den betegnet som foreldet og er kun tilgjengelig som _tråd for bakoverkompatibilitet.

Du bør bruke det høyere nivået træ modul for applikasjoner du har tenkt å distribuere. Trådmodulen er kun dekket her for pedagogiske formål.

Trådmodulen

Syntaksen for å lage en ny tråd ved hjelp av denne modulen er som følger:

thread.start_new_thread(function_name, arguments)

Greit, nå har du dekket den grunnleggende teorien for å begynne å kode. Så åpne din IDLE eller en notisblokk og skriv inn følgende:

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))

Lagre filen og trykk F5 for å kjøre programmet. Hvis alt ble gjort riktig, er dette utgangen du bør se:

Trådmodulen

Du vil lære mer om løpsforhold og hvordan du håndterer dem i de kommende avsnittene.

Trådmodulen

KODE FORKLARING

  1. Disse setningene importerer time- og thread-modulen, som brukes til å håndtere utførelsen og forsinkelsen av Python tråder.
  2. Her har du definert en funksjon kalt thread_test, som vil bli kalt av start_ny_tråd metode. Funksjonen kjører en while-løkke i fire iterasjoner og skriver ut navnet på tråden som kalte den. Når iterasjonen er fullført, skriver den ut en melding som sier at tråden er ferdigkjørt.
  3. Dette er hoveddelen av programmet ditt. Her ringer du ganske enkelt start_ny_tråd metoden med thread_test funksjon som et argument. Dette vil opprette en ny tråd for funksjonen du sender som et argument og begynne å kjøre den. Merk at du kan erstatte denne (thread_test) med en hvilken som helst annen funksjon du vil kjøre som en tråd.

Trådemodulen

Denne modulen er den overordnede implementeringen av threading i Python og de facto-standarden for håndtering av flertrådede applikasjoner. Den tilbyr et bredt spekter av funksjoner sammenlignet med trådmodulen.

Strukturen til gjengemodulen

Strukturen til gjengemodulen

Her er en liste over noen nyttige funksjoner definert i denne modulen:

Funksjonsnavn Tekniske beskrivelser
activeCount() Returnerer tellingen av Tråd gjenstander som fortsatt er levende.
gjeldende tråd() Returnerer gjeldende objekt i trådklassen.
oppregne () Viser alle aktive trådobjekter.
isDaemon() Returnerer sant hvis tråden er en demon.
er i live() Returnerer sant hvis tråden fortsatt er i live.
Tråd Klasse metoder
start() Starter aktiviteten til en tråd. Den må bare kalles én gang for hver tråd fordi den vil gi en kjøretidsfeil hvis den kalles opp flere ganger.
løpe() Denne metoden angir aktiviteten til en tråd og kan overstyres av en klasse som utvider trådklassen.
bli med() Den blokkerer kjøringen av annen kode til tråden som join()-metoden ble kalt blir avsluttet.

Bakgrunn: Trådklassen

Før du begynner å kode flertrådede programmer ved hjelp av trådmodulen, er det viktig å forstå trådklassen. Trådklassen er den primære klassen som definerer malen og operasjonene til en tråd i Python.

Den vanligste måten å lage en flertrådet Python Applikasjonen er å deklarere en klasse som utvider Thread-klassen og overstyrer dens run()-metode.

Thread-klassen, oppsummert, betyr en kodesekvens som kjører i en separat tråden av kontroll.

Så når du skriver en flertrådsapp, vil du gjøre følgende:

  1. definere en klasse som utvider trådklassen
  2. Overstyr __init__ konstruktør
  3. Overstyr løpe() metode

Når et trådobjekt er laget, vil start() metoden kan brukes til å starte utførelsen av denne aktiviteten, og bli med() metoden kan brukes til å blokkere all annen kode til den gjeldende aktiviteten er ferdig.

La oss nå prøve å bruke threading-modulen til å implementere det forrige eksemplet ditt. Start opp igjen IDLE og skriv inn følgende:

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()

Dette vil være utdata når du utfører koden ovenfor:

Bakgrunn: Trådklassen

KODE FORKLARING

Bakgrunn: Trådklassen

  1. Denne delen er den samme som i vårt forrige eksempel. Her importerer du tids- og trådmodulen, som brukes til å håndtere utførelse og forsinkelser av Python tråder.
  2. I denne biten oppretter du en klasse kalt threadtester, som arver eller utvider Tråd klassen til trådmodulen. Dette er en av de vanligste måtene å lage tråder på i PythonDu bør imidlertid bare overstyre konstruktøren og løpe() metoden i appen din. Som du kan se i kodeeksemplet ovenfor, er __init__ metode (konstruktør) har blitt overstyrt. På samme måte har du også overstyrt løpe() metode. Den inneholder koden du vil kjøre i en tråd. I dette eksemplet har du kalt thread_test() funksjonen.
  3. Dette er thread_test()-metoden, som tar verdien av i som et argument, reduserer det med 1 ved hver iterasjon, og går gjennom resten av koden i løkker til i blir 0. I hver iterasjon skriver den ut navnet på den tråden som kjører for øyeblikket og sover i ventesekunder (som også tas som et argument).
  4. thread1 = threadtester(1, "First Thread", 1) Her lager vi en tråd og sender de tre parameterne som vi erklærte i __init__. Den første parameteren er id-en til tråden, den andre parameteren er trådens navn, og den tredje parameteren er telleren, som bestemmer hvor mange ganger while-løkken skal kjøres.
  5. thread2.start() Start-metoden brukes til å starte kjøringen av en tråd. Internt kaller start()-funksjonen run()-metoden til klassen din.
  6. thread3.join() join()-metoden blokkerer kjøringen av annen kode og venter til tråden den ble kalt er ferdig.

Som du allerede vet, har tråder som er i samme prosess tilgang til minnet og dataene til den prosessen. Som et resultat, hvis mer enn én tråd prøver å endre eller få tilgang til dataene samtidig, kan det snike seg inn feil.

I neste avsnitt vil du se de ulike typene komplikasjoner som kan dukke opp når tråder får tilgang til data og den kritiske delen uten å sjekke eksisterende tilgangstransaksjoner.

Vranglås og løpsforhold

Før vi lærer om vranglåser og kappløpsbetingelser, vil det være nyttig å forstå noen grunnleggende definisjoner knyttet til samtidig programmering:

  • Kritisk del: Det er et kodefragment som åpner eller endrer delte variabler og må utføres som en atomtransaksjon.
  • Kontekstbytte: Det er prosessen en CPU følger for å lagre tilstanden til en tråd før den bytter fra en oppgave til en annen, slik at den kan gjenopptas fra samme punkt senere.

Låsesperre

Låsesperre er det mest fryktede problemet utviklere møter når de skriver samtidige/flertrådete applikasjoner i PythonDen beste måten å forstå vranglåser på er å bruke det klassiske informatikk-eksempelproblemet kjent som Servering Philosophers problem.

Problemstillingen for matfilosofer er som følger:

Fem filosofer sitter ved et rundt bord med fem tallerkener med spaghetti (en type pasta) og fem gafler, som vist i diagrammet.

Servering Philosophers problem

Servering Philosophers problem

Til enhver tid må en filosof enten spise eller tenke.

Dessuten må en filosof ta de to gaflene ved siden av ham (dvs. venstre og høyre gafler) før han kan spise spaghetti. Problemet med dødlås oppstår når alle fem filosofene plukker opp sine høyre gafler samtidig.

Siden hver av filosofene har én gaffel, vil de alle vente på at de andre legger gaffelen fra seg. Som et resultat vil ingen av dem kunne spise spaghetti.

På samme måte, i et samtidig system, oppstår en vranglås når forskjellige tråder eller prosesser (filosofer) prøver å skaffe de delte systemressursene (gafler) samtidig. Som et resultat får ingen av prosessene en sjanse til å utføres ettersom de venter på en annen ressurs som holdes av en annen prosess.

Løpsbetingelser

En kappløpstilstand er en uønsket tilstand i et program som oppstår når et system utfører to eller flere operasjoner samtidig. Tenk deg for eksempel denne enkle for-løkken:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Hvis du oppretter n antall tråder som kjører denne koden samtidig, kan du ikke bestemme verdien av i (som deles av trådene) når programmet er ferdig med å kjøre. Dette er fordi trådene i et ekte flertrådingsmiljø kan overlappe hverandre, og verdien av i som ble hentet og endret av en tråd kan endres i mellomtiden når en annen tråd åpner den.

Dette er de to hovedklassene av problemer som kan oppstå i en flertrådet eller distribuert Python applikasjon. I neste avsnitt lærer du hvordan du løser dette problemet ved å synkronisere tråder.

Synchroniserende tråder

For å håndtere løpsforhold, vranglås og andre trådbaserte problemer, gir gjengemodulen Låse gjenstand. Tanken er at når en tråd vil ha tilgang til en spesifikk ressurs, får den en lås for den ressursen. Når en tråd låser en bestemt ressurs, kan ingen annen tråd få tilgang til den før låsen frigjøres. Som et resultat vil endringene i ressursen være atomære, og raseforhold vil bli avverget.

En lås er en synkroniseringsprimitiv på lavt nivå implementert av _tråd modul. En lås kan til enhver tid være i en av to tilstander: låst or ulåst. Den støtter to metoder:

  1. erverve(): Når låsetilstanden er ulåst, vil kall av acquire()-metoden endre tilstanden til locked og returnere. Hvis tilstanden derimot er locked, blokkeres kallet av acquire() inntil release()-metoden kalles av en annen tråd.
  2. utgivelse(): Release()-metoden brukes til å sette tilstanden til ulåst, dvs. å frigjøre en lås. Den kan kalles av hvilken som helst tråd, ikke nødvendigvis den som skaffet låsen.

Her er et eksempel på bruk av låser i appene dine. Start opp IDLE og skriv følgende:

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()

Nå, trykk F5. Du bør se en utgang som dette:

Synchronizing tråder

KODE FORKLARING

Synchronizing tråder

  1. Her oppretter du ganske enkelt en ny lås ved å ringe threading.Lock() fabrikkfunksjon. Internt returnerer Lock() en forekomst av den mest effektive betonglåsklassen som vedlikeholdes av plattformen.
  2. I den første setningen anskaffer du låsen ved å kalle förvärv()-metoden. Når låsen er innvilget skriver du ut "lås anskaffet" til konsollen. Når all koden du vil at tråden skal kjøre, er fullført, slipper du låsen ved å kalle release()-metoden.

Teorien er grei, men hvordan vet du at låsen virkelig fungerte? Hvis du ser på resultatet, vil du se at hver av print-setningene skriver ut nøyaktig én linje om gangen. Husk at i et tidligere eksempel var resultatet fra print tilfeldig fordi flere tråder hadde tilgang til print()-metoden samtidig. Her kalles print-funksjonen bare etter at låsen er tilegnet. Så resultatet vises én om gangen og linje for linje.

Bortsett fra låser, Python støtter også noen andre mekanismer for å håndtere trådsynkronisering, som listet opp nedenfor:

  1. RLåser
  2. Semaphores
  3. Forhold
  4. hendelser, og
  5. Barrierer

Global Tolkelås (og hvordan man håndterer det)

Før du går inn i detaljene Pythons GIL, la oss definere noen begreper som vil være nyttige for å forstå den kommende delen:

  1. CPU-bundet kode: dette refererer til enhver kode som vil bli direkte utført av CPU-en.
  2. I/O-bundet kode: dette kan være hvilken som helst kode som får tilgang til filsystemet gjennom operativsystemet.
  3. CPython: det er referansen gjennomføring of Python og kan beskrives som tolken skrevet i C og Python (programmeringsspråk).

Hva er GIL i Python?

Global Interpreter Lock (GIL) in Python er en prosesslås eller en mutex som brukes når man håndterer prosessene. Den sørger for at én tråd kan få tilgang til en bestemt ressurs om gangen, og den forhindrer også bruk av objekter og bytekoder samtidig. Dette gagner programmene med én tråd med en ytelsesøkning. GIL i Python er veldig enkel og lett å implementere.

En lås kan brukes for å sikre at bare én tråd har tilgang til en bestemt ressurs på et gitt tidspunkt.

En av funksjonene i Python er at den bruker en global lås på hver tolkeprosess, som betyr at hver prosess behandler Python tolke seg selv som en ressurs.

For eksempel, anta at du har skrevet en Python Program som bruker to tråder til å utføre både CPU- og 'I/O'-operasjoner. Når du kjører dette programmet, skjer dette:

  1. Ocuco Python tolk oppretter en ny prosess og starter trådene.
  2. Når tråd-1 begynner å kjøre, vil den først anskaffe GIL og låse den.
  3. Hvis tråd-2 ønsker å kjøre nå, må den vente på at GIL blir utgitt selv om en annen prosessor er ledig.
  4. Anta nå at tråd-1 venter på en I/O-operasjon. På dette tidspunktet vil den frigjøre GIL, og tråd-2 vil skaffe den.
  5. Etter å ha fullført I/O-operasjonene, hvis tråd-1 ønsker å kjøre nå, må den igjen vente på at GIL blir utgitt av tråd-2.

På grunn av dette kan bare én tråd få tilgang til tolken om gangen, noe som betyr at det bare vil være én tråd som kjører Python kode på et gitt tidspunkt.

Dette er greit i en enkeltkjerneprosessor fordi den ville bruke tidsdeling (se den første delen av denne veiledningen) for å håndtere trådene. Men i tilfelle flerkjerneprosessorer vil en CPU-bundet funksjon som kjører på flere tråder ha en betydelig innvirkning på programmets effektivitet, siden det faktisk ikke vil bruke alle tilgjengelige kjerner samtidig.

Hvorfor var GIL nødvendig?

CPython søppelinnsamleren bruker en effektiv minnehåndteringsteknikk kjent som referansetelling. Slik fungerer det: Hvert objekt i Python har et referanseantall, som økes når det tilordnes et nytt variabelnavn eller legges til en container (som tupler, lister osv.). På samme måte reduseres referanseantall når referansen går utenfor omfanget eller når del-setningen kalles. Når referanseantallet til et objekt når 0, blir det søppelhentet, og det tildelte minnet frigjøres.

Men problemet er at referanseantallvariabelen er utsatt for kappløpsbetingelser som alle andre globale variabler. For å løse dette problemet, har utviklerne av Python bestemte seg for å bruke den globale tolkelåsen. Det andre alternativet var å legge til en lås til hvert objekt, noe som ville ha resultert i vranglåser og økt overhead fra acquire()- og release()-kall.

Derfor er GIL en betydelig begrensning for flertrådet Python programmer som kjører tunge CPU-bundne operasjoner (noe som effektivt gjør dem enkelttrådede). Hvis du vil bruke flere CPU-kjerner i applikasjonen din, bruk multi modul i stedet.

Spørsmål og svar

Bruk tråding for I/O-bundet arbeid som nettverkskall, filtilgang eller databasespørringer, der tråder venter på eksterne ressurser. Bruk flerprosessering for CPU-bundet arbeid som tunge beregninger, fordi det kjører på flere kjerner og omgår GIL.

Ikke som standard. PEP 703 introduserte en valgfri innebygd frittstående funksjon. Python 3.13 som deaktiverer GIL, og Python 3.14 fortsetter med det. Standardversjoner leverer fortsatt GIL, så de fleste programmene fortsetter å kjøre nøyaktig som før.

En daemontråd kjører i bakgrunnen og blokkerer ikke programmet fra å avslutte. Når det bare er daemontråder igjen, Python slår dem av og avslutter. Sett en med thread.daemon = True før start() kalles.

Thread-klassen returnerer ikke run()-resultatet direkte. Lagre verdien på thread-objektet eller en delt kø, eller bruk concurrent.futures.ThreadPoolExecutor, hvis submit()-metode returnerer en Future du leser med result().

ThreadPoolExecutor er en hjelper på høyt nivå som administrerer en pool av arbeidstråder for deg. Du sender inn oppgaver med submit() eller map(), og den håndterer trådoppretting, gjenbruk og opprydding, og returnerer Future-objekter som inneholder hvert resultat.

Du kan opprette hundrevis eller tusenvis av tråder, men GIL lar bare én utføre Python bytekode når som helst. De overlapper fortsatt under I/O-venting, så mange samtidige tråder hjelper I/O-bundne programmer mye mer enn CPU-bundne.

Ja, hovedsakelig for I/O-bundne trinn som lasting av data, kalling av API-er eller lesing av filer. Tung trening er CPU-bundet, så biblioteker som NumPy og PyTorch frigi GIL i C-kode eller stol på multiprosessering for ekte parallellisme.

Ja. GitHub Copilot fullfører automatisk vanlige mønstre som trådunderklasser, låsebruk og ThreadPoolExecutor-oppsett fra en kommentar eller et funksjonsnavn. Se alltid gjennom den genererte koden for kappløpsbetingelser og korrekt låsehåndtering før du stoler på den.

Oppsummer dette innlegget med: