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.
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:
- Ocuco tråden modul, og
- 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:
Du vil lære mer om løpsforhold og hvordan du håndterer dem i de kommende avsnittene.
KODE FORKLARING
- Disse setningene importerer time- og thread-modulen, som brukes til å håndtere utførelsen og forsinkelsen av Python tråder.
- 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.
- 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
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:
- definere en klasse som utvider trådklassen
- Overstyr __init__ konstruktør
- 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:
KODE FORKLARING
- 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.
- 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.
- 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).
- 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.
- thread2.start() Start-metoden brukes til å starte kjøringen av en tråd. Internt kaller start()-funksjonen run()-metoden til klassen din.
- 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
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:
- 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.
- 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:
KODE FORKLARING
- 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.
- 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:
- RLåser
- Semaphores
- Forhold
- hendelser, og
- 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:
- CPU-bundet kode: dette refererer til enhver kode som vil bli direkte utført av CPU-en.
- I/O-bundet kode: dette kan være hvilken som helst kode som får tilgang til filsystemet gjennom operativsystemet.
- 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:
- Ocuco Python tolk oppretter en ny prosess og starter trådene.
- Når tråd-1 begynner å kjøre, vil den først anskaffe GIL og låse den.
- Hvis tråd-2 ønsker å kjøre nå, må den vente på at GIL blir utgitt selv om en annen prosessor er ledig.
- 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.
- 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.









