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

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

Hvad er en tråd?

En tråd er en enhed for udførelse 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 grundlæggende 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 ind 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. Multitråde kan udføres individuelt, mens de deler deres procesressourcer. Formålet med multithreading er at køre flere opgaver og funktionsceller 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

Overvej dette scenario for at forstå processer og tråde: 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 ​​programmet, som nu kører, kaldes processen.

Hver proces vil have 2 grundlæggende komponenter:

  • Koden
  • Dataene

Nu kan en proces indeholde en eller flere kaldede underdele tråde. Dette afhænger af OS-arkitekturen. Du kan tænke på en tråd som en del af processen, som kan udføres separat af operativsystemet.

Med andre ord er det en strøm af instruktioner, som kan køres uafhængigt af operativsystemet. Tråde i en enkelt proces deler dataene fra denne proces og er designet til at arbejde sammen for at lette parallelitet.

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 tutorial vil du primært fokusere på implementering flertrådede applikationer med python. Der er to hovedmoduler, som kan bruges til at håndtere tråde i Python:

  1. tråd modul, og
  2. gevindskæring modul

Men i python er der også noget, der hedder en global fortolkerlås (GIL). Det giver ikke mulighed for meget præstationsforøgelse 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 undervisningsformå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:

Trådmodulet

Du vil lære mere om løbsforhold og hvordan du håndterer dem i de kommende afsnit

Trådmodulet

KODE FORKLARING

  1. Disse udsagn importerer tids- og trådmodulet, som bruges til at håndtere udførelsen og forsinkelsen af Python tråde.
  2. 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 gentagelsen er fuldført, udskriver den en meddelelse, der siger, at tråden er afsluttet.
  3. Dette er hovedafsnittet i dit program. Her ringer du blot til start_ny_tråd metode med tråd_test fungerer som et argument. Dette vil oprette en ny tråd for den funktion, du sender som argument, og begynde 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ådemodulet

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

Opbygning af trådningsmodul
Opbygning af trådningsmodul

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

Funktionsnavn Description
activeCount() Returnerer optællingen af Tråd genstande, der stadig er i live
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 flertrådede programmer ved hjælp af trådmodulet, er det afgørende at forstå trådklassen. Trådklassen er den primære klasse, som definerer skabelonen og operationerne for en tråd i python.

Den mest almindelige måde at oprette en multithreaded python-applikation på er at erklære en klasse, som 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:

  1. definere en klasse, som udvider trådklassen
  2. Tilsidesæt __i det__ konstruktør
  3. Tilsidesæt løb() metode

Når et trådobjekt er blevet lavet, vil den Start() metode kan bruges til at begynde 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 trådningsmodulet til at implementere dit tidligere eksempel. Igen, fyr op for 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:

Baghistorie: Trådklassen

KODE FORKLARING

Baghistorie: Trådklassen

  1. Denne del er den samme som vores tidligere eksempel. Her importerer du tids- og trådmodulet, som bruges til at håndtere udførelse og forsinkelser af Python tråde.
  2. I denne bit opretter du en klasse kaldet threadtester, som arver eller forlænger Tråd klasse af gevindmodulet. Dette er en af ​​de mest almindelige måder at oprette tråde på i python. Du 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().
  3. Dette er metoden thread_test() som tager værdien af i som et argument, reducerer det med 1 ved hver iteration og går gennem resten af ​​koden, indtil i bliver 0. I hver iteration udskriver det navnet på den aktuelt eksekverende tråd og sover i vente sekunder (hvilket også tages som et argument ).
  4. 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.
  5. thread2.start()Startmetoden bruges til at starte udførelsen af ​​en tråd. Internt kalder start()-funktionen run()-metoden for din klasse.
  6. 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 de tråde, der er i samme proces, adgang til hukommelsen og dataene for denne proces. Som et resultat, hvis mere end én tråd forsøger at ændre eller få adgang til dataene samtidigt, kan der snige sig fejl ind.

I næste afsnit vil du se de forskellige slags komplikationer, der kan dukke op, når tråde får adgang til data og kritiske sektioner uden at tjekke for eksisterende adgangstransaktioner.

Deadlocks og Race-forhold

Før du lærer om dødvande og løbsforhold, 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 tilgår eller ændrer delte variabler og skal udføres som en atomtransaktion.
  • Context SwitchDet 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

Fastlåsning er det mest frygtede problem, som udviklere står over for, når de skriver samtidige/multithreadede applikationer i python. Den bedste måde at forstå dødvande på er ved at bruge det klassiske computervidenskabelige eksempelproblem kendt som Dining Philosophers problem.

Problemformuleringen 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

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 race-tilstand er en uønsket tilstand af et program, som opstår, når et system udfører to eller flere operationer samtidigt. Overvej for eksempel dette simple for loop:

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

Hvis du opretter n antal 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 afslutter eksekveringen. Dette skyldes, at i et rigtigt multithreading-miljø kan trådene 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 multithreaded eller distribueret python-applikation. I det næste afsnit lærer du, hvordan du overvinder 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. På ethvert givet tidspunkt kan en lås være i en af ​​2 tilstande: låst or ulåst. Det understøtter to metoder:

  1. erhverve()Når låsetilstanden er låst op, vil kald af förvärv()-metoden ændre tilstanden til låst og returnere. Men hvis tilstanden er låst, blokeres kaldet til erhvervelse() indtil release()-metoden kaldes af en anden tråd.
  2. 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å brug af låse i dine apps. Fyr op for 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:

Synchronizing tråde

KODE FORKLARING

Synchronizing tråde

  1. 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.
  2. 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 printsætningerne udskriver præcis én linje ad gangen. Husk, at i et tidligere eksempel var output fra print tilfældige, fordi flere tråde fik adgang til print()-metoden på samme tid. Her kaldes printfunktionen 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 at håndtere trådsynkronisering som angivet nedenfor:

  1. RLåser
  2. Semaphores
  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 termer, der vil være nyttige til at forstå det kommende afsnit:

  1. CPU-bundet kode: dette refererer til ethvert stykke kode, som vil blive udført 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 of 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, mens man håndterer processerne. Det sikrer, at én 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 de enkelt-trådede programmer i en ydelsesforøgelse. 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 fortolkerproces, hvilket betyder, at hver proces behandler selve pythonfortolkeren som en ressource.

Antag for eksempel, at du har skrevet et python-program, som bruger to tråde til at udføre både CPU- og 'I/O'-operationer. Når du kører dette program, sker der følgende:

  1. Python-fortolkeren opretter en ny proces og afføder trådene
  2. Når tråd-1 begynder at køre, henter den først GIL og låser den.
  3. Hvis thread-2 vil køre nu, skal den vente på, at GIL'en frigives, selvom en anden processor er ledig.
  4. 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.
  5. 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 til enhver tid få adgang til fortolkeren, 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 det ville være at bruge tidsudskæring (se det første afsnit af denne vejledning) til at håndtere trådene. Men i tilfælde af multi-core processorer, vil en CPU-bundet funktion, der udføres på flere tråde, have en betydelig indvirkning 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 referencetælling. Sådan fungerer det: Hvert objekt i python har et referenceantal, som øges, når det tildeles et nyt variabelnavn eller føjes til en beholder (som tupler, lister osv.). Ligeledes reduceres referenceantallet, når referencen går uden for scope, eller når del-sætningen kaldes. Når referencetallet for et objekt når 0, bliver det opsamlet skrald, og den tildelte hukommelse frigives.

Men problemet er, at referencetællingsvariablen er tilbøjelig til raceforhold 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, hvilket ville have resulteret i deadlocks og øget overhead fra acquisit() og release() kald.

Derfor er GIL en væsentlig begrænsning for flertrådede python-programmer, der kører tunge CPU-bundne operationer (effektivt gør dem enkelttrådede). Hvis du ønsker at gøre brug af flere CPU-kerner i din applikation, skal du bruge multibearbejdning modul i stedet for.

Resumé

  • Python understøtter 2 moduler til multithreading:
    1. __tråd modul: Det giver en implementering på lavt niveau til gevindskæring og er forældet.
    2. gevindskæringsmodul: 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ådningsmodulet skal du gøre følgende:
    1. Opret en klasse, der udvider Tråd klasse.
    2. Tilsidesæt dens konstruktør (__init__).
    3. Tilsidesæt dens løb() fremgangsmåde.
    4. Opret et objekt af denne klasse.
  • En tråd kan udføres ved at kalde Start() fremgangsmåde.
  • tilslutte() metoden kan bruges til at blokere andre tråde, indtil denne tråd (den som join blev kaldt) afslutter eksekveringen.
  • En race-tilstand opstår, når flere tråde får adgang til eller ændrer en delt ressource på samme tid.
  • Det kan undgås ved Synchroniserende tråde.
  • Python understøtter 6 måder at synkronisere tråde på:
    1. Låse
    2. RLåser
    3. Semaphores
    4. Betingelser
    5. Begivenheder, og
    6. Barrierer
  • Låse tillader kun en bestemt tråd, som har erhvervet låsen, at komme ind i den kritiske sektion.
  • En lås har 2 primære metoder:
    1. erhverve(): Den indstiller låsetilstanden til Låst. Hvis der kaldes på et låst objekt, blokerer det, indtil ressourcen er fri.
    2. frigøre(): Den indstiller låsetilstanden til ulåst og vender tilbage. Hvis du kalder på et ulåst objekt, returnerer det falsk.
  • Den globale tolkelås er en mekanisme, hvorigennem kun 1 CPython tolkeproces kan udføres ad gangen.
  • Det blev brugt til at lette referencetællingsfunktionaliteten i CPythons' skraldemand.
  • For at gøre Python apps med tunge CPU-bundne operationer, bør du bruge multiprocessing-modulet.