Multithreading inn Python med Eksempel: Lær GIL i Python
Hva er en tråd?
En tråd er en enhet for utførelse ved samtidig programmering. Multithreading er en teknikk som lar en CPU utføre mange oppgaver i en prosess samtidig. Disse trådene kan kjøres individuelt mens de deler prosessressursene sine.
Hva er en prosess?
En prosess er i utgangspunktet programmet i utførelse. Når du starter et program på datamaskinen din (som en nettleser eller tekstredigerer), 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 sitt datarom med hovedtråden som gjør informasjonsdeling og kommunikasjon innenfor tråder enkel og effektiv. Tråder er lettere enn prosesser. Multitråder kan kjøres individuelt mens de deler prosessressursene sine. Hensikten med multithreading er å kjøre flere oppgaver og funksjonsceller 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, vurder dette scenariet: En .exe-fil på datamaskinen din er et program. Når du åpner den, laster OS den inn i minnet, og CPU kjører den. Forekomsten av programmet som nå kjører kalles prosessen.
Hver prosess vil ha 2 grunnleggende komponenter:
- Koden
- Dataen
Nå kan en prosess inneholde en eller flere underdeler kalt tråder. Dette avhenger av OS-arkitekturen. 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 i en enkelt prosess deler dataene fra den prosessen og er designet for å fungere sammen for å lette 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 multiprosessering så vel som multithreading. I denne opplæringen vil du først og fremst fokusere på implementering flertråds applikasjoner med python. Det er to hovedmoduler som kan brukes til å håndtere tråder inn Python:
- Ocuco tråden modul, og
- Ocuco træ moduler
Men i python er det også noe som kalles en global tolklås (GIL). Det tillater ikke mye ytelsesgevinst 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øpsforholdene og hvordan du håndterer dem i de kommende delene
KODE FORKLARING
- Disse setningene importerer tids- og trådmodulen som brukes til å håndtere utførelse og forsinkelse 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, skrives det ut en melding som sier at tråden er fullført.
- Dette er hoveddelen av programmet ditt. Her ringer du ganske enkelt start_ny_tråd metoden med thread_test fungerer som et argument. Dette vil opprette en ny tråd for funksjonen du sender som argument og begynne å utføre den. Merk at du kan erstatte denne (tråd_test) med en hvilken som helst annen funksjon du vil kjøre som en tråd.
Trådemodulen
Denne modulen er høynivåimplementeringen av tråding i python og de facto-standarden for administrasjon av flertrådede applikasjoner. Den gir et bredt spekter av funksjoner sammenlignet med trådmodulen.
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 i live |
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 å bruke trådmodulen, er det avgjørende å forstå trådklassen. Trådklassen er primærklassen som definerer malen og operasjonene til en tråd i python.
Den vanligste måten å lage en multithreaded python-applikasjon på er å erklære 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:
- definer 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 trådmodulen for å implementere ditt forrige eksempel. Igjen, fyr opp 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 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 klasse av gjengemodulen. Dette er en av de vanligste måtene å lage tråder på i python. Du 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 til i blir 0. I hver iterasjon skriver den ut navnet på tråden som kjører for øyeblikket og sover i vente sekunder (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()Startmetoden brukes til å starte kjøringen av en tråd. Internt kaller start()-funksjonen opp 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ådene 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 seksjon vil du se de forskjellige typene komplikasjoner som kan dukke opp når tråder får tilgang til data og kritisk seksjon uten å se etter eksisterende tilgangstransaksjoner.
Vranglås og løpsforhold
Før du lærer om vranglås og løpsforhold, vil det være nyttig å forstå noen grunnleggende definisjoner knyttet til samtidig programmering:
- Critical SectionDet er et fragment av kode som får tilgang til eller modifiserer delte variabler og må utføres som en atomtransaksjon.
- Context SwitchDet er prosessen som en CPU følger for å lagre tilstanden til en tråd før den endres fra en oppgave til en annen, slik at den kan gjenopptas fra samme punkt senere.
Låsesperre
Låsesperre er det mest fryktede problemet som utviklere møter når de skriver samtidige/flertrådede applikasjoner i python. Den beste måten å forstå vranglås på er ved å bruke det klassiske informatikkeksempelproblemet kjent som Servering Philosophers problem.
Problemstillingen for matfilosofer er som følger:
Fem filosofer sitter på et rundt bord med fem plater spaghetti (en type pasta) og fem gafler, som vist i diagrammet.
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 løpstilstand er en uønsket tilstand i et program som oppstår når et system utfører to eller flere operasjoner samtidig. Tenk for eksempel på dette som er enkelt for loop:
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øringen. Dette er fordi i et ekte flertrådsmiljø kan trådene overlappe hverandre, og verdien av i som ble hentet og modifisert av en tråd kan endres mellom når en annen tråd får tilgang til den.
Dette er de to hovedklassene av problemer som kan oppstå i en multithreaded eller distribuert python-applikasjon. I neste avsnitt vil du lære hvordan du overvinner 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. Til enhver tid kan en lås være i en av to tilstander: låst or ulåst. Den støtter to metoder:
- erverve()Når låsetilstanden er låst opp, vil oppkalling av förvärv()-metoden endre tilstanden til låst og returnere. Imidlertid, hvis tilstanden er låst, blokkeres kallet for å få() 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. Fyr opp din 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å utskriften, vil du se at hver av utskriftssetningene skriver ut nøyaktig én linje om gangen. Husk at i et tidligere eksempel var utdataene fra print tilfeldige fordi flere tråder hadde tilgang til print()-metoden samtidig. Her kalles utskriftsfunksjonen først etter at låsen er anskaffet. Så utgangene vises en om gangen og linje for linje.
Bortsett fra låser, støtter python også noen andre mekanismer for å håndtere trådsynkronisering som oppført nedenfor:
- RLåser
- Semaphores
- Forhold
- hendelser, og
- Barrierer
Global Tolkelås (og hvordan man håndterer det)
Før vi går inn på detaljene i pythons GIL, la oss definere noen begreper som vil være nyttige for å forstå den kommende delen:
- CPU-bundet kode: dette refererer til ethvert kodestykke som vil bli utført direkte av CPU.
- 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) i python er en prosesslås eller en mutex som brukes mens du 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 de entrådede programmene i en ytelsesøkning. GIL i python er veldig enkelt 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 tolkprosess, noe som betyr at hver prosess behandler selve pytontolkeren som en ressurs.
Anta for eksempel at du har skrevet et 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:
- Python-tolken oppretter en ny prosess og skaper 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 til enhver tid, noe som betyr at det kun vil være én tråd som kjører python-kode på et gitt tidspunkt.
Dette er greit i en enkeltkjerneprosessor fordi det ville være å bruke tidsskjæring (se den første delen av denne opplæringen) for å håndtere trådene. Men i tilfelle multi-core prosessorer, 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 de tilgjengelige kjernene samtidig.
Hvorfor var GIL nødvendig?
CPython garbage collector bruker en effektiv minnehåndteringsteknikk kjent som referansetelling. Slik fungerer det: Hvert objekt i python har et referanseantall, som økes når det tildeles et nytt variabelnavn eller legges til en beholder (som tupler, lister osv.). På samme måte reduseres referanseantallet når referansen går utenfor scope eller når del-setningen kalles. Når referansetellingen til et objekt når 0, samles det opp søppel, og det tildelte minnet frigjøres.
Men problemet er at referansetellingsvariabelen er utsatt for raseforhold som enhver annen global variabel. For å løse dette problemet bestemte utviklerne av python seg for å bruke den globale tolkelåsen. Det andre alternativet var å legge til en lås til hvert objekt som ville ha resultert i vranglås og økt overhead fra anrop () og slipp().
Derfor er GIL en betydelig begrensning for flertrådede python-programmer som kjører tunge CPU-bundne operasjoner (effektivt gjør dem enkelttrådede). Hvis du vil bruke flere CPU-kjerner i applikasjonen din, bruk multi modul i stedet.
Sammendrag
- Python støtter 2 moduler for multithreading:
- __tråd modul: Den gir en implementering på lavt nivå for gjenging og er foreldet.
- gjengemodul: Det gir en implementering på høyt nivå for multithreading og er gjeldende standard.
- For å opprette en tråd ved hjelp av trådingsmodulen, må du gjøre følgende:
- Lag en klasse som utvider Tråd klasse.
- Overstyr konstruktøren (__init__).
- Overstyr dens løpe() metoden.
- Lag et objekt av denne klassen.
- En tråd kan kjøres ved å kalle start() metoden.
- Ocuco bli med() metoden kan brukes til å blokkere andre tråder til denne tråden (den som sammenføyningen ble kalt) fullfører utførelse.
- En rasetilstand oppstår når flere tråder får tilgang til eller endrer en delt ressurs samtidig.
- Det kan unngås ved Synchroniserende tråder.
- Python støtter 6 måter å synkronisere tråder på:
- Låser
- RLåser
- Semaphores
- Forhold
- hendelser, og
- Barrierer
- Låser lar bare en bestemt tråd som har ervervet låsen komme inn i den kritiske delen.
- En lås har 2 primære metoder:
- erverve(): Den setter låsetilstanden til låst. Hvis det kalles på et låst objekt, blokkeres det til ressursen er ledig.
- utgivelse(): Den setter låsetilstanden til ulåst og returnerer. Hvis det kalles på et ulåst objekt, returnerer det false.
- Den globale tolkelåsen er en mekanisme som bare 1 CPython tolkeprosessen kan utføres om gangen.
- Den ble brukt for å lette referansetellingsfunksjonaliteten til CPythons sin søppelsamler.
- For å gjøre Python apper med tunge CPU-bundne operasjoner, bør du bruke multiprosesseringsmodulen.