Multithreading i Python med Global Interpreter Lock (GIL) eksempel

Indholdsfortegnelse:

Anonim

Python-programmeringssproget giver dig mulighed for at bruge multiprocessing eller multithreading. I denne vejledning lærer du at skrive multitrådede applikationer i Python.

Hvad er en tråd?

En tråd er en eksektionsenhed ved samtidig programmering. Multithreading er en teknik, der gør det muligt for en CPU at udføre mange opgaver i en 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 teksteditor), opretter operativsystemet en proces.

Hvad er multithreading i Python?

Multithreading i Python- programmering er en velkendt teknik, hvor flere tråde i en proces deler deres datarum med hovedtråden, hvilket gør informationsdeling og kommunikation inden for tråde let og effektiv. Tråde er lettere end processer. Multi-tråde kan udføres individuelt, mens de deler deres procesressourcer. Formålet med multithreading er at køre flere opgaver og fungere celler 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 skal du overveje dette scenarie: En .exe-fil på din computer er et program. Når du åbner det, indlæser OS det i hukommelsen, og CPU'en udfører det. Forekomsten af ​​det program, der nu kører, kaldes processen.

Hver proces har to grundlæggende komponenter:

  • Koden
  • Dataene

Nu kan en proces indeholde en eller flere underdele kaldet tråde. Dette afhænger af OS-arkitekturen. Du kan tænke på en tråd som et afsnit af processen, som 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 dataene for denne proces og er designet til at arbejde sammen for at lette parallelisme.

I denne vejledning lærer du,

  • Hvad er en tråd?
  • Hvad er en proces?
  • Hvad er multithreading?
  • Hvad er multiprocessing?
  • Python Multithreading vs Multiprocessing
  • Hvorfor bruge Multithreading?
  • Python MultiThreading
  • Tråd- og trådmodulerne
  • Trådmodulet
  • Trådningsmodulet
  • Dødlåse og race betingelser
  • Synkronisering af tråde
  • Hvad er GIL?
  • Hvorfor var der brug for GIL?

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 multiprocessing såvel som multithreading. I denne vejledning vil du primært fokusere på implementering af multitrådede applikationer med python. Der er to hovedmoduler, der kan bruges til at håndtere tråde i Python:

  1. Den tråd modul, og
  2. Den threading modul

Imidlertid er der i python også noget, der kaldes en global tolkelås (GIL). Det tillader ikke meget præstationsforøgelse og kan endda reducere ydeevnen for nogle multitrådede applikationer. Du lærer alt om det i de kommende afsnit af denne vejledning.

Tråd- og trådmodulerne

De to moduler, som du vil lære om i denne vejledning, er trådmodulet og trådmodulet .

Trådmodulet er dog længe udfaset. Fra og med Python 3 er det blevet betegnet som forældet og er kun tilgængeligt som __thread for bagudkompatibilitet.

Du skal bruge threading- modulet på højere niveau til applikationer, som du agter at implementere. Trådmodulet er kun 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 om at starte kodning. Så åbn din IDLE eller et notesblok, og skriv følgende:

import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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 det output, du skal se:

Du vil lære mere om race betingelser og hvordan man håndterer dem i de kommende sektioner

KODEKLARING

  1. Disse udsagn importerer tids- og trådmodulet, der bruges til at håndtere udførelse og forsinkelse af Python-tråde.
  2. Her har du defineret en funktion kaldet thread_test, som kaldes ved start_new_thread- metoden. Funktionen kører et stykke løb i fire iterationer og udskriver navnet på den tråd, der kaldte den. Når gentagelsen er færdig, udskriver den en besked, der siger, at tråden er færdig med udførelsen.
  3. Dette er den vigtigste del af dit program. Her kalder du simpelthen metoden start_new_thread med thread_test- funktionen som et argument.

    Dette opretter en ny tråd til den funktion, du sender som argument, og begynder at udføre den. Bemærk, at du kan erstatte denne (tråd _ test) med enhver anden funktion, som du vil køre som en tråd.

Trådningsmodulet

Dette modul er implementeringen på højt niveau af threading i python og de facto-standarden til styring af multitrådede applikationer. Det giver en bred vifte af funktioner sammenlignet med trådmodulet.

Trådmodulets struktur

Her er en liste over nogle nyttige funktioner defineret i dette modul:

Funktionsnavn Beskrivelse
activeCount () Returnerer antallet af trådobjekter , der stadig er i live
currentThread () Returnerer det aktuelle objekt i trådklassen.
opregne () Viser alle aktive trådgenstande.
isDaemon () Returnerer sandt, hvis tråden er en dæmon.
er i live() Returnerer sandt, hvis tråden stadig er i live.
Trådklassemetoder
Start() Starter aktiviteten af ​​en tråd. Det skal kun kaldes en gang for hver tråd, fordi det kaster en runtime-fejl, hvis den 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, hvor metoden join () blev kaldt, afsluttes.

Baggrund: Trådklassen

Før du begynder at kode multitrådede programmer ved hjælp af threading-modulet, er det vigtigt at forstå Thread-klassen. Thread-klassen er den primære klasse, der definerer skabelonen og operationerne af en tråd i python.

Den mest almindelige måde at oprette en flertrådet pythonapplikation på er at erklære en klasse, der udvider trådklassen og tilsidesætter den kørte () metode.

Thread klasse i resumé, betegner en kodesekvens, der kører i en separat tråd af kontrol.

Så når du skriver en multitrådet app, skal du gøre følgende:

  1. definer en klasse, der udvider trådklassen
  2. Tilsidesæt __init__- konstruktøren
  3. Tilsidesæt run () -metoden

Når et trådobjekt er oprettet, kan start () metoden bruges til at begynde udførelsen af ​​denne aktivitet, og metoden join () kan bruges til at blokere al anden kode, indtil den aktuelle aktivitet er færdig.

Lad os nu prøve at bruge trådmodulet til at implementere dit tidligere eksempel. Igen skal du slå din IDLE op og indtaste følgende:

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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 output, når du udfører ovenstående kode:

KODEKLARING

  1. Denne del er den samme som vores tidligere eksempel. Her importerer du det tids- og trådmodul, der bruges til at håndtere udførelse og forsinkelser af Python-tråde.
  2. I denne bit, skal du oprette en klasse kaldet threadtester, der arver eller udvider Tråd klasse af gevindet modulet. Dette er en af ​​de mest almindelige måder at oprette tråde på python. Du bør dog kun tilsidesætte konstruktøren og metoden run () i din app. Som du kan se i ovenstående kodeeksempel er __init__- metoden (konstruktør) blevet tilsidesat.

    På samme måde har du også tilsidesat metoden run () . Den indeholder den kode, du vil udføre i en tråd. I dette eksempel har du kaldt thread_test () -funktionen.

  3. Dette er metoden thread_test (), der tager værdien af i som et argument, reducerer den med 1 ved hver iteration og løber gennem resten af ​​koden, indtil jeg bliver 0. I hver iteration udskrives navnet på den aktuelt udførende tråd og sover i ventesekunder (hvilket også tages som et argument).
  4. thread1 = threadtester (1, "Første tråd", 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, der bestemmer, hvor mange gange mens løkken skal køres.

  5. thread2.start ()

    Startmetoden bruges til at starte udførelsen af ​​en tråd. Internt kalder start () -funktionen run () -metoden i din klasse.

  6. thread3.join ()

    Metoden join () blokerer udførelsen af ​​anden kode og venter, indtil den tråd, den blev kaldt på, er færdig.

Som du allerede ved, har trådene i samme proces adgang til hukommelsen og dataene i den proces. Som et resultat, hvis mere end en tråd forsøger at ændre eller få adgang til dataene samtidigt, kan der muligvis krybe fejl.

I det næste afsnit vil du se de forskellige slags komplikationer, der kan vises, når tråde får adgang til data og kritisk sektion uden at kontrollere for eksisterende adgangstransaktioner.

Dødlåse og race betingelser

Før du lærer om deadlocks og race betingelser, vil det være nyttigt at forstå et par grundlæggende definitioner relateret til samtidig programmering:

  • Kritisk sektion

    Det er et fragment af kode, der får adgang til eller ændrer delte variabler og skal udføres som en atomtransaktion.

  • Kontekstskift

    Det er den proces, som en CPU følger for at gemme en tråds tilstand, før den skifter fra en opgave til en anden, så den kan genoptages fra samme punkt senere.

Fastlåsning

Deadlocks er det mest frygtede problem, som udviklere står over for, når de skriver samtidige / multitrådede applikationer i python. Den bedste måde at forstå blokeringer på er ved hjælp af det klassiske problem inden for datalogi, kendt som Dining Philosophers Problem.

Problemstillingen for spisefilosoffer er som følger:

Fem filosoffer sidder på et rundt bord med fem plader spaghetti (en type pasta) og fem gafler, som vist i diagrammet.

Dining Philosophers Problem

Til enhver tid skal en filosof enten spise eller tænke.

Desuden skal en filosof tage de to gafler, der støder op til ham (dvs. venstre og højre gafler), før han kan spise spaghetti. Problemet med dødvande opstår, når alle fem filosoffer henter deres højre gafler samtidigt.

Da hver filosof har én gaffel, vil de alle vente på, at de andre lægger deres gaffel ned. Som et resultat vil ingen af ​​dem være i stand til at spise spaghetti.

På samme måde opstår der i et sammenfaldende system en blokering, når forskellige tråde eller processer (filosoffer) forsøger at tilegne sig de delte systemressourcer (gafler) på samme tid. Som et resultat får ingen af ​​processerne en chance for at udføre, da de venter på en anden ressource, som en anden proces har.

Race betingelser

En løbetilstand er en uønsket tilstand af et program, der opstår, når et system udfører to eller flere operationer samtidigt. Overvej for eksempel dette simpelt til loop:

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

Hvis du opretter et antal tråde, der kører denne kode på én gang, kan du ikke bestemme værdien af ​​i (som deles af tråde), når programmet afslutter udførelsen. Dette skyldes, at trådene i et ægte multithreading-miljø kan overlappe hinanden, og værdien af ​​i, som blev hentet og ændret af en tråd, kan ændre sig imellem, når en anden tråd får adgang til den.

Dette er de to hovedklasser af problemer, der kan opstå i en multitrådet eller distribueret pythonapplikation. I det næste afsnit lærer du, hvordan du løser dette problem ved at synkronisere tråde.

Synkronisering af tråde

For at håndtere race betingelser, deadlocks og andre trådbaserede problemer leverer threading-modulet Lock- objektet. Ideen er, at når en tråd ønsker adgang til en bestemt 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 frigøres. Som et resultat vil ændringer i ressourcen være atomare, og race betingelser vil blive afværget.

En lås er en primitiv synkroniseringsprimitiv implementeret af __thread- modulet. Til enhver tid kan en lås være i en af ​​to tilstande: låst eller ulåst. Det understøtter to metoder:

  1. erhverve()

    Når låsetilstanden er låst op, vil opkald til erhvervelsesmetoden () ændre tilstanden til låst og returnere. Men hvis tilstanden er låst, blokeres opkaldet til at erhverve (), indtil frigivelsesmetoden () kaldes af en anden tråd.

  2. frigøre()

    Release () -metoden bruges til at indstille tilstanden til ulåst, dvs. frigøre en lås. Det kan kaldes af en hvilken som helst tråd, ikke nødvendigvis den, der erhvervede låsen.

Her er et eksempel på brug af låse i dine apps. Tænd din IDLE og skriv følgende:

import threadinglock = 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()

Hit nu F5. Du skal se en output som denne:

KODEKLARING

  1. Her opretter du simpelthen en ny lås ved at kalde threading.Lock () fabriksfunktionen. Internt returnerer Lock () en forekomst af den mest effektive betonlåseklasse, der opretholdes af platformen.
  2. I den første erklæring erhverver du låsen ved at kalde erhverve () metoden. Når låsen er tildelt, udskriver du "lås erhvervet" til konsollen. Når al den kode, som du vil have tråden til at køre, er udført, frigøres låsen ved at kalde frigivelsesmetoden ().

Teorien er fint, men hvordan ved du, at låsen virkelig virkede? Hvis du ser på output, vil du se, at hver af udskriftserklæringerne udskriver nøjagtigt en linje ad gangen. Husk at i et tidligere eksempel var output fra print tilfældigt, fordi flere tråde fik adgang til print () -metoden på samme tid. Her kaldes udskrivningsfunktionen først, efter at låsen er erhvervet. Så udgangene vises en ad gangen og linje for linje.

Bortset fra låse understøtter python også nogle andre mekanismer til håndtering af trådsynkronisering som angivet nedenfor:

  1. RLocks
  2. Semaforer
  3. Betingelser
  4. Begivenheder og
  5. Barrierer

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

Før vi går ind i detaljerne i pythons GIL, lad os definere et par udtryk, der vil være nyttige til forståelse af det kommende afsnit:

  1. CPU-bundet kode: dette refererer til ethvert stykke kode, der udføres direkte af CPU'en.
  2. I / O-bundet kode: dette kan være en hvilken som helst kode, der får adgang til filsystemet gennem OS
  3. CPython: Det er referencen implementering af Python og kan beskrives som tolken skrevet i C og Python (programmeringssprog).

Hvad er GIL i Python?

Global Interpreter Lock (GIL) i python er en proceslås eller en mutex, der bruges under behandling af processerne. Det sørger for, at en tråd kan få adgang til en bestemt ressource ad gangen, og det forhindrer også brugen af ​​objekter og bytekoder på én gang. Dette gavner programmerne med en enkelt tråd i en præstationsforøgelse. GIL i python er meget enkel og nem at implementere.

En lås kan bruges til at sikre, at kun en 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 tolkeproces, hvilket betyder, at hver proces behandler selve pythontolken som en ressource.

Antag for eksempel, at du har skrevet et 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 det her:

  1. Pythontolken opretter en ny proces og gyder trådene
  2. Når tråd-1 begynder at køre, vil den først erhverve GIL og låse den.
  3. Hvis tråd-2 ønsker at udføre nu, bliver den nødt til at vente på, at GIL frigives, selvom en anden processor er gratis.
  4. Antag nu, at tråd-1 venter på en I / O-operation. På dette tidspunkt frigiver det GIL, og thread-2 vil erhverve det.
  5. Efter at have afsluttet I / O-opsætningerne, hvis thread-1 ønsker at udføre nu, bliver det igen nødt til at vente på, at GIL frigives af thread-2.

På grund af dette kan kun en tråd få adgang til tolken til enhver tid, hvilket betyder, at der kun vil være en tråd, der udfører python-kode på et givet tidspunkt.

Dette er okay i en enkeltkerneprocessor, fordi den bruger tidsskæring (se første afsnit i 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 bruger alle de tilgængelige kerner på samme tid.

Hvorfor var der brug for GIL?

CPython affaldssamler bruger en effektiv hukommelsesstyringsteknik kendt som referencetælling. Sådan fungerer det: Hvert objekt i python har et referencetal, som øges, når det tildeles et nyt variabelnavn eller føjes til en container (som tupler, lister osv.). Ligeledes mindskes referencetallet, når referencen går uden for rækkevidden, eller når del-sætningen kaldes. Når et objekts referencetælling når 0, indsamles det skrald, og den tildelte hukommelse frigøres.

Men problemet er, at referencetællingsvariablen er tilbøjelig til race betingelser som enhver anden global variabel. For at løse dette problem besluttede udviklerne af python at bruge den globale tolkelås. Den anden mulighed var at tilføje en lås til hvert objekt, som ville have resulteret i blokeringer og øget overhead fra erhvervelse () og frigivelse () opkald.

Derfor er GIL en væsentlig begrænsning for flertrådede pythonprogrammer, der kører tunge CPU-bundne operationer (hvilket effektivt gør dem til enkeltgevind). Hvis du vil bruge flere CPU-kerner i din applikation, skal du bruge multiprocessing- modulet i stedet.

Resumé

  • Python understøtter 2 moduler til multithreading:
    1. __thread modul: Det giver en implementering på lavt niveau til threading og er forældet.
    2. threading-modul : Det giver en implementering på højt niveau til multithreading og er den nuværende standard.
  • For at oprette en tråd ved hjælp af trådmodulet skal du gøre følgende:
    1. Opret en klasse, der udvider trådklassen .
    2. Tilsidesæt dens konstruktør (__init__).
    3. Tilsidesæt metoden run () .
    4. Opret et objekt fra denne klasse.
  • En tråd kan udføres ved at kalde start () -metoden.
  • Metoden join () kan bruges til at blokere andre tråde, indtil denne tråd (den, som join blev kaldt) er færdig med udførelsen.
  • En løbetilstand opstår, når flere tråde får adgang til eller ændrer en delt ressource på samme tid.
  • Det kan undgås ved at synkronisere tråde.
  • Python understøtter 6 måder at synkronisere tråde:
    1. Låse
    2. RLocks
    3. Semaforer
    4. Betingelser
    5. Begivenheder og
    6. Barrierer
  • Låse tillader kun en bestemt tråd, som har erhvervet låsen, at komme ind i det kritiske afsnit.
  • En lås har to primære metoder:
    1. erhverve () : Det indstiller låsetilstanden til låst. Hvis der kaldes på et låst objekt, blokerer det, indtil ressourcen er gratis.
    2. release () : Det indstiller låsetilstanden til ulåst og vender tilbage. Hvis der kaldes på et ulåst objekt, returnerer det falsk.
  • Den globale tolkelås er en mekanisme, gennem hvilken kun 1 CPython-tolkeproces kan udføres ad gangen.
  • Det blev brugt til at lette referencetællingsfunktionaliteten hos CPythons affaldssamler.
  • For at oprette Python-apps med tunge CPU-bundne operationer skal du bruge multiprocessing-modulet.