Multithreading ind Python med Eksempel: Lær GIL i Python
⚡ Smart opsummering
Multithreading ind Python kører flere tråde i én proces, så de deler hukommelse og arbejder samtidigt. Trådmodulet opretter og administrerer disse tråde, mens Global Interpreter Lock begrænser ægte parallelisme, hvilket gør teknikken bedst egnet til input/output-bundne opgaver.

Python Programmeringssprog giver dig mulighed for at bruge multiprocessering eller multithreading. I denne vejledning lærer du, hvordan du skriver multithreadede applikationer i Python.
Hvad er en tråd?
En tråd er en udførelsesenhed i samtidig programmering. Multithreading er en teknik, der tillader en CPU at udføre mange opgaver i én proces på samme tid. Disse tråde kan udføres individuelt, mens de deler deres procesressourcer.
Hvad er en proces?
En proces er dybest set det program, der udføres. Når du starter et program på din computer (som en browser eller et tekstredigeringsprogram), opretter operativsystemet en proces.
Hvad er Multithreading i Python?
Multithreading ind Python Programmering er en velkendt teknik, hvor flere tråde i en proces deler deres dataområde med hovedtråden, hvilket gør informationsdeling og kommunikation inden for tråde nem og effektiv. Tråde er lettere end processer. Flere tråde kan udføres individuelt, mens de deler deres procesressourcer. Formålet med multithreading er at køre flere opgaver og funktioner på samme tid.
Hvad er multiprocessing?
multiprocessing giver dig mulighed for at køre flere ikke-relaterede processer samtidigt. Disse processer deler ikke deres ressourcer og kommunikerer gennem IPC.
Python Multithreading vs Multiprocessing
For at forstå processer og tråde, overvej dette scenarie: En .exe-fil på din computer er et program. Når du åbner den, indlæser operativsystemet den i hukommelsen, og CPU'en udfører den. Den instans af programmet, der nu kører, kaldes processen.
Enhver proces har to grundlæggende komponenter:
- Code
- Dataene
Nu kan en proces indeholde en eller flere kaldede underdele tråde. Dette afhænger af operativsystemets arkitektur. Du kan tænke på en tråd som en del af processen, der kan udføres separat af operativsystemet.
Med andre ord er det en strøm af instruktioner, der kan køres uafhængigt af operativsystemet. Tråde inden for en enkelt proces deler data fra den pågældende proces og er designet til at arbejde sammen for at fremme parallelisme.
Hvorfor bruge Multithreading?
Multithreading giver dig mulighed for at opdele en applikation i flere underopgaver og køre disse opgaver samtidigt. Hvis du bruger multithreading korrekt, kan din applikationshastighed, ydeevne og gengivelse forbedres.
Python multithreading
Python understøtter konstruktioner til både multiprocessering og multithreading. I denne vejledning vil du primært fokusere på implementering af flertrådede applikationer med PythonDer er to hovedmoduler, der kan bruges til at håndtere tråde i Python:
- tråd modul, og
- gevindskæring modul
Imidlertid i Python, der er også noget, der kaldes en global interpreter lock (GIL). Den giver ikke mulighed for megen ydeevneforbedring og kan endda reducere ydelsen af nogle flertrådede applikationer. Du vil lære alt om det i de kommende sektioner af denne tutorial.
Modulerne Tråd og Trådning
De to moduler, som du vil lære om i denne tutorial er gevind modul og gevindskæringsmodul.
Trådmodulet har dog længe været forældet. Starter med Python 3, er den betegnet som forældet og er kun tilgængelig som _tråd for bagudkompatibilitet.
Du bør bruge det højere niveau gevindskæring modul til applikationer, som du har til hensigt at implementere. Trådmodulet er kun blevet dækket her til uddannelsesmæssige formål.
Trådmodulet
Syntaksen for at oprette en ny tråd ved hjælp af dette modul er som følger:
thread.start_new_thread(function_name, arguments)
Okay, nu har du dækket den grundlæggende teori for at begynde at kode. Så åben din IDLE eller en notesblok og indtast 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))
Gem filen og tryk på F5 for at køre programmet. Hvis alt blev gjort korrekt, er dette output, du skal se:
Du vil lære mere om løbsforhold og hvordan du håndterer dem i de kommende afsnit.
KODE FORKLARING
- Disse udsagn importerer time- og thread-modulerne, som bruges til at håndtere udførelsen og forsinkelsen af Python tråde.
- Her har du defineret en funktion kaldet tråd_test, som vil blive kaldt af start_ny_tråd metode. Funktionen kører en while-løkke i fire iterationer og udskriver navnet på den tråd, der kaldte den. Når iterationen er færdig, udskriver den en besked, der siger, at tråden er færdig med at køre.
- Dette er hovedafsnittet i dit program. Her ringer du blot til start_ny_tråd metode med tråd_test funktion som et argument. Dette vil oprette en ny tråd for den funktion, du sender som et argument, og begynde at udføre den. Bemærk, at du kan erstatte denne (thread_test) med enhver anden funktion, du vil køre som en tråd.
Trådemodulet
Dette modul er den overordnede implementering af threading i Python og de facto standarden for håndtering af multithreaded-applikationer. Den tilbyder en bred vifte af funktioner sammenlignet med thread-modulet.
Opbygning af trådningsmodul
Her er en liste over nogle nyttige funktioner defineret i dette modul:
| Funktionsnavn | Beskrivelse |
|---|---|
| activeCount() | Returnerer optællingen af Tråd genstande, der stadig er levende. |
| nuværende tråd() | Returnerer det aktuelle objekt i trådklassen. |
| opregne() | Viser alle aktive trådobjekter. |
| isDaemon() | Returnerer sandt, hvis tråden er en dæmon. |
| er i live() | Returnerer sandt, hvis tråden stadig er i live. |
| Tråd Klasse metoder | |
| Start() | Starter aktiviteten af en tråd. Det må kun kaldes én gang for hver tråd, fordi det vil give en runtime-fejl, hvis det kaldes flere gange. |
| løb() | Denne metode angiver aktiviteten af en tråd og kan tilsidesættes af en klasse, der udvider trådklassen. |
| tilslutte() | Det blokerer udførelsen af anden kode, indtil tråden, som join()-metoden blev kaldt, bliver afsluttet. |
Baghistorie: Trådklassen
Før du begynder at kode multithreadede programmer ved hjælp af threading-modulet, er det afgørende at forstå Thread-klassen. Trådklassen er den primære klasse, der definerer skabelonen og operationerne for en tråd i Python.
Den mest almindelige måde at oprette en multithreaded på Python Applikationen er at deklarere en klasse, der udvider Thread-klassen og tilsidesætter dens run()-metode.
Sammenfattende betegner trådklassen en kodesekvens, der kører i en separat tråd af kontrol.
Så når du skriver en multithreaded app, vil du gøre følgende:
- Definer en klasse, der udvider Thread-klassen
- Tilsidesæt __i det__ konstruktør
- Tilsidesæt løb() metode
Når et trådobjekt er blevet lavet, vil den Start() metoden kan bruges til at starte udførelsen af denne aktivitet, og tilslutte() metoden kan bruges til at blokere al anden kode, indtil den aktuelle aktivitet afsluttes.
Lad os nu prøve at bruge threading-modulet til at implementere dit tidligere eksempel. Igen, tænd din IDLE og indtast 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 outputtet, når du udfører ovenstående kode:
KODE FORKLARING
- Denne del er den samme som i vores tidligere eksempel. Her importerer du tids- og trådmodulet, som bruges til at håndtere udførelsen og forsinkelserne af Python tråde.
- I denne bit opretter du en klasse kaldet threadtester, som arver eller forlænger Tråd klasse af trådmodulet. Dette er en af de mest almindelige måder at oprette tråde på PythonDu bør dog kun tilsidesætte konstruktøren og løb() metode i din app. Som du kan se i ovenstående kodeeksempel, er __i det__ metode (konstruktør) er blevet tilsidesat. På samme måde har du også tilsidesat løb() metode. Den indeholder den kode, som du vil udføre inde i en tråd. I dette eksempel har du kaldt funktionen thread_test().
- Dette er thread_test()-metoden, som tager værdien af i som et argument, reducerer det med 1 ved hver iteration, og looper gennem resten af koden, indtil i bliver 0. I hver iteration udskriver den navnet på den aktuelt kørende tråd og sover i ventesekunder (hvilket også tages som et argument).
- thread1 = threadtester(1, "First Thread", 1) Her opretter vi en tråd og sender de tre parametre, som vi erklærede i __init__. Den første parameter er trådens id, den anden parameter er trådens navn, og den tredje parameter er tælleren, som bestemmer hvor mange gange while-løkken skal køre.
- thread2.start() Start-metoden bruges til at starte udførelsen af en tråd. Internt kalder start()-funktionen run()-metoden for din klasse.
- thread3.join() join()-metoden blokerer udførelsen af anden kode og venter, indtil den tråd, som den blev kaldt, afsluttes.
Som du allerede ved, har tråde i den samme proces adgang til hukommelsen og dataene i den pågældende proces. Som følge heraf kan der snige sig fejl ind, hvis mere end én tråd forsøger at ændre eller tilgå dataene samtidigt.
I næste afsnit vil du se de forskellige typer komplikationer, der kan opstå, når tråde tilgår data og den kritiske sektion uden at kontrollere eksisterende adgangstransaktioner.
Deadlocks og Race-forhold
Før man lærer om deadlocks og race conditions, vil det være nyttigt at forstå et par grundlæggende definitioner relateret til samtidig programmering:
- Kritisk sektion: Det er et kodefragment, der tilgår eller ændrer delte variabler og skal udføres som en atomar transaktion.
- Kontekstskift: Det er den proces, som en CPU følger for at gemme tilstanden af en tråd, før den skifter fra en opgave til en anden, så den kan genoptages fra samme punkt senere.
Fastlåsning
Fastlåsning er det mest frygtede problem, som udviklere står over for, når de skriver samtidige/multitrådede applikationer i PythonDen bedste måde at forstå fastlåste situationer på er ved at bruge det klassiske datalogiske eksempelproblem kendt som Dining Philosophers problem.
Problemformuleringen for spisefilosoffer er som følger:
Fem filosoffer sidder ved et rundt bord med fem tallerkener spaghetti (en slags pasta) og fem gafler, som vist i diagrammet.
Dining Philosophers problem
På ethvert givet tidspunkt skal en filosof enten spise eller tænke.
Desuden skal en filosof tage de to gafler ved siden af ham (dvs. venstre og højre gafler), før han kan spise spaghettien. Problemet med dødvande opstår, når alle fem filosoffer tager deres højre gafler op samtidigt.
Da hver af filosofferne har en gaffel, vil de alle vente på, at de andre lægger deres gaffel. Som følge heraf vil ingen af dem være i stand til at spise spaghetti.
Tilsvarende opstår der i et samtidig system et dødvande, når forskellige tråde eller processer (filosoffer) forsøger at erhverve de delte systemressourcer (gafler) på samme tid. Som følge heraf får ingen af processerne en chance for at eksekvere, da de venter på en anden ressource, der holdes af en anden proces.
Race betingelser
En kapløbstilstand er en uønsket tilstand i et program, der opstår, når et system udfører to eller flere operationer samtidigt. For eksempel, overvej denne simple for-løkke:
i=0; # a global variable for x in range(100): print(i) i+=1;
Hvis du opretter n antallet af tråde, der kører denne kode på én gang, kan du ikke bestemme værdien af i (som deles af trådene), når programmet er færdigt. Dette skyldes, at trådene i et rigtigt multithreading-miljø kan overlappe hinanden, og værdien af i, der blev hentet og ændret af en tråd, kan ændre sig indimellem, når en anden tråd tilgår den.
Dette er de to hovedklasser af problemer, der kan opstå i en multithreaded eller distribueret Python applikation. I næste afsnit lærer du, hvordan du løser dette problem ved at synkronisere tråde.
Synchroniserende tråde
For at håndtere løbsforhold, deadlocks og andre trådbaserede problemer, giver gevindmodulet Lås objekt. Ideen er, at når en tråd vil have adgang til en specifik ressource, får den en lås til den ressource. Når en tråd låser en bestemt ressource, kan ingen anden tråd få adgang til den, før låsen er frigivet. Som følge heraf vil ændringerne af ressourcen være atomare, og raceforhold vil blive afværget.
En lås er en synkroniseringsprimitiv på lavt niveau implementeret af _tråd modul. En lås kan til enhver tid være i en af to tilstande: låst or ulåst. Det understøtter to metoder:
- erhverve(): Når låsetilstanden er ulåst, vil et kald af acquire()-metoden ændre tilstanden til låst og returnere. Men hvis tilstanden er låst, blokeres kaldet af acquire(), indtil release()-metoden kaldes af en anden tråd.
- frigøre(): Release()-metoden bruges til at indstille tilstanden til ulåst, dvs. at frigive en lås. Det kan kaldes af enhver tråd, ikke nødvendigvis den, der har erhvervet låsen.
Her er et eksempel på brugen af låse i dine apps. Tænd 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()
Tryk nu på F5. Du bør se et output som dette:
KODE FORKLARING
- Her opretter du blot en ny lås ved at ringe til threading.Lock() fabriksfunktion. Internt returnerer Lock() en forekomst af den mest effektive konkrete Lock-klasse, der vedligeholdes af platformen.
- I det første udsagn erhverver du låsen ved at kalde förvärv()-metoden. Når låsen er givet, udskriver du "lås erhvervet" til konsollen. Når al den kode, som du vil have tråden til at køre, er afsluttet, frigiver du låsen ved at kalde release()-metoden.
Teorien er fin, men hvordan ved du, at låsen virkelig virkede? Hvis du ser på outputtet, vil du se, at hver af print-sætningerne udskriver præcis én linje ad gangen. Husk, at i et tidligere eksempel var outputtet fra print tilfældigt, fordi flere tråde tilgik print()-metoden på samme tid. Her kaldes print-funktionen først, efter at låsen er aktiveret. Så outputtet vises et ad gangen og linje for linje.
Udover låse, Python understøtter også nogle andre mekanismer til at håndtere trådsynkronisering, som angivet nedenfor:
- RLåser
- Semaphores
- Betingelser
- Begivenheder, og
- Barrierer
Global Tolkelås (og hvordan man håndterer det)
Før vi går i detaljer med Python's GIL, lad os definere et par termer, der vil være nyttige til at forstå det kommende afsnit:
- CPU-bundet kode: dette refererer til ethvert stykke kode, der vil blive udført direkte af CPU'en.
- I/O-bundet kode: dette kan være enhver kode, der tilgår filsystemet via operativsystemet.
- CPython: det er referencen implementering of Python og kan beskrives som tolken skrevet i C og Python (programmeringssprog).
Hvad er GIL i Python?
Global Interpreter Lock (GIL) in Python er en proceslås eller en mutex, der bruges under håndtering af processer. Den sikrer, at én tråd kan tilgå en bestemt ressource ad gangen, og den forhindrer også brugen af objekter og bytekoder på én gang. Dette gavner enkelttrådede programmer med en forbedret ydeevne. GIL i Python er meget enkel og nem at implementere.
En lås kan bruges til at sikre, at kun én tråd har adgang til en bestemt ressource på et givet tidspunkt.
En af funktionerne i Python er, at den bruger en global lås på hver fortolkningsproces, hvilket betyder, at hver proces behandler Python fortolke sig selv som en ressource.
For eksempel, lad os sige, at du har skrevet en Python Program, der bruger to tråde til at udføre både CPU- og 'I/O'-operationer. Når du udfører dette program, sker der følgende:
- Python interpreteren opretter en ny proces og starter trådene.
- Når tråd-1 begynder at køre, henter den først GIL og låser den.
- Hvis thread-2 vil køre nu, skal den vente på, at GIL'en frigives, selvom en anden processor er ledig.
- Antag nu, at tråd-1 venter på en I/O-operation. På dette tidspunkt vil den frigive GIL, og tråd-2 vil erhverve den.
- Efter at have fuldført I/O-operationerne, hvis tråd-1 ønsker at udføre nu, vil den igen skulle vente på, at GIL'en frigives af tråd-2.
På grund af dette kan kun én tråd tilgå fortolkeren ad gangen, hvilket betyder, at der kun vil være én tråd, der udfører Python kode på et givet tidspunkt.
Dette er i orden i en single-core processor, fordi den ville bruge time slicing (se første afsnit af denne vejledning) til at håndtere trådene. I tilfælde af multi-core processorer vil en CPU-bundet funktion, der udføres på flere tråde, dog have en betydelig indflydelse på programmets effektivitet, da det faktisk ikke vil bruge alle de tilgængelige kerner på samme tid.
Hvorfor var der brug for GIL?
CPython garbage collector bruger en effektiv hukommelseshåndteringsteknik kendt som referenceoptælling. Sådan fungerer det: Hvert objekt i Python har et referenceantal, som øges, når det tildeles et nyt variabelnavn eller tilføjes til en container (som tupler, lister osv.). Ligeledes reduceres referenceantallet, når referencen kommer uden for omfanget, eller når del-sætningen kaldes. Når referenceantallet for et objekt når 0, indsamles det som garbage collected, og den tildelte hukommelse frigøres.
Men problemet er, at reference count-variablen er tilbøjelig til kapløbsbetingelser ligesom enhver anden global variabel. For at løse dette problem har udviklerne af Python besluttede at bruge den globale fortolkerlås. Den anden mulighed var at tilføje en lås til hvert objekt, hvilket ville have resulteret i deadlocks og øget overhead fra acquire() og release() kald.
Derfor er GIL en betydelig begrænsning for multithreaded Python programmer, der kører tunge CPU-bundne operationer (hvilket effektivt gør dem single-threaded). Hvis du vil bruge flere CPU-kerner i din applikation, skal du bruge multibearbejdning modul i stedet for.








