Primer večnitnosti v Pythonu z globalnim zaklepanjem tolmačev (GIL)

Kazalo:

Anonim

Programski jezik python vam omogoča uporabo večprocesorske obdelave ali večnitnosti.V tej vadnici boste izvedeli, kako v Pythonu pisati večnitne aplikacije.

Kaj je nit?

Nit je enota za izločanje pri sočasnem programiranju. Večnitnost je tehnika, ki CPU omogoča izvajanje več nalog enega procesa hkrati. Te niti se lahko izvajajo posamično, medtem ko delijo svoje procesne vire.

Kaj je postopek?

Proces je v bistvu program v izvedbi. Ko v računalniku zaženete aplikacijo (na primer brskalnik ali urejevalnik besedil), operacijski sistem ustvari postopek.

Kaj je večnitnost v Pythonu?

Večnitnost pri programiranju na Pythonu je dobro znana tehnika, pri kateri več niti v procesu deli svoj podatkovni prostor z glavno nitjo, kar omogoča enostavno in učinkovito izmenjavo informacij in komunikacijo znotraj niti. Niti so lažji od procesov. Več niti se lahko izvajajo posamično, medtem ko delijo svoje procesne vire. Namen večnitnosti je hkrati zagnati več nalog in funkcijskih celic.

Kaj je večprocesorska obdelava?

Večprocesorska obdelava omogoča sočasno izvajanje več nepovezanih procesov. Ti procesi si ne delijo svojih virov in komunicirajo prek IPC.

Python Multithreading vs Multiprocessing

Če želite razumeti procese in niti, upoštevajte ta scenarij: Datoteka .exe v računalniku je program. Ko ga odprete, ga OS naloži v pomnilnik, CPU pa ga izvrši. Primerek programa, ki se zdaj izvaja, se imenuje postopek.

Vsak postopek bo imel dve temeljni komponenti:

  • Koda
  • Podatki

Zdaj lahko postopek vsebuje enega ali več poddelov, imenovanih niti. To je odvisno od arhitekture OS,. Nit lahko predstavljate kot odsek procesa, ki ga lahko operacijski sistem izvede posebej.

Z drugimi besedami, gre za tok navodil, ki jih lahko OS zažene neodvisno. Niti znotraj enega procesa delijo podatke o tem procesu in so zasnovani tako, da skupaj olajšajo vzporednost.

V tej vadnici boste izvedeli,

  • Kaj je nit?
  • Kaj je postopek?
  • Kaj je večnitnost?
  • Kaj je večprocesorska obdelava?
  • Python Multithreading vs Multiprocessing
  • Zakaj uporabljati Multithreading?
  • Python MultiThreading
  • Moduli Thread in Threading
  • Modul niti
  • Navojni modul
  • Zastoji in dirkalni pogoji
  • Sinhronizacija niti
  • Kaj je GIL?
  • Zakaj je bil potreben GIL?

Zakaj uporabljati Multithreading?

Večnitnost omogoča razčlenitev aplikacije na več pod nalog in izvajanje teh nalog hkrati. Če pravilno uporabljate večnitnost, je mogoče izboljšati hitrost, zmogljivost in upodabljanje aplikacije.

Python MultiThreading

Python podpira konstrukcije tako za večprocesorsko kot tudi za večnitnost. V tej vadnici se boste osredotočili predvsem na izvajanje večnitnih aplikacij s pythonom. Obstajata dva glavna modula, ki ju je mogoče uporabiti za obdelavo niti v Pythonu:

  1. Modul niti in
  2. Navojev modul

Vendar pa v pythonu obstaja tudi nekaj, kar se imenuje globalna zaklepanje tolmačev (GIL). Ne omogoča večjega povečanja zmogljivosti in lahko celo zmanjša delovanje nekaterih večnitnih aplikacij. Vse o tem boste izvedeli v prihodnjih razdelkih te vadnice.

Moduli Thread in Threading

Dva modula, o katerih boste izvedeli v tej vadnici, sta modul niti in modul navojev .

Vendar je nitni modul že dolgo zastarel. Od Pythona 3 je bil označen kot zastarel in je za povratno združljivost dostopen samo kot __thread .

Za aplikacije, ki jih nameravate uvesti, bi morali uporabiti modul navojev na višji ravni . Modul niti je tukaj zajet samo v izobraževalne namene.

Modul niti

Sintaksa za ustvarjanje nove niti z uporabo tega modula je naslednja:

thread.start_new_thread(function_name, arguments)

V redu, zdaj ste zajeli osnovno teorijo za začetek kodiranja. Torej odprite IDLE ali beležko in vnesite naslednje:

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))

Shranite datoteko in pritisnite F5, da zaženete program. Če je bilo vse narejeno pravilno, bi morali videti to:

Več o dirkalnih pogojih in kako ravnati z njimi boste izvedeli v naslednjih odsekih

POJASNILO KODE

  1. Ti stavki uvažajo modul časa in niti, ki se uporabljata za obdelavo izvajanja in zavlačevanja niti Python.
  2. Tu ste definirali funkcijo z imenom thread_test, ki jo bo poklicala metoda start_new_thread . Funkcija zažene zanko while za štiri ponovitve in izpiše ime niti, ki jo je poklicala. Ko je iteracija končana, natisne sporočilo, da je nit končala izvajanje.
  3. To je glavni del vašega programa. Tukaj preprosto pokličite start_new_thread metodo z thread_test funkcijo kot argument.

    To bo ustvarilo novo nit funkcije, ki jo posredujete kot argument, in jo začelo izvajati. Upoštevajte, da lahko to (nit _ test) zamenjate s katero koli drugo funkcijo, ki jo želite zagnati kot nit.

Navojni modul

Ta modul je izvedba navojev na visoki ravni v pythonu in dejanski standard za upravljanje večnitnih aplikacij. Ponuja širok spekter funkcij v primerjavi z modulom niti.

Struktura navojnega modula

Tu je seznam nekaterih uporabnih funkcij, opredeljenih v tem modulu:

Ime funkcije Opis
activeCount () Vrne število predmetov niti, ki so še živi
currentThread () Vrne trenutni objekt razreda Thread.
enumerate () Seznam vseh aktivnih predmetov niti.
isDaemon () Vrne true, če je nit demon.
isAlive () Vrne true, če je nit še vedno živa.
Metode razreda niti
začetek () Začne dejavnost niti. Za vsako nit mora biti poklican samo enkrat, ker bo pri večkratnem klicu vrgel napako med izvajanjem.
teči () Ta metoda označuje aktivnost niti in jo lahko preglasi razred, ki razširja razred Thread.
pridruži se () Blokira izvajanje druge kode, dokler se nit, na kateri je bila klicana metoda join (), ne konča.

Ozadje: Razred niti

Preden začnete kodirati večnitne programe z uporabo navojnega modula, je ključnega pomena razumevanje razreda Thread. Razred niti je primarni razred, ki definira predlogo in operacije niti v pythonu.

Najpogostejši način za ustvarjanje večnitne aplikacije python je razglasitev razreda, ki razširja razred Thread in preglasi metodo run ().

Razred Thread v povzetku pomeni zaporedje kode, ki se izvaja v ločeni niti nadzora.

Torej, ko pišete večnitno aplikacijo, boste storili naslednje:

  1. definirajte razred, ki razširja razred Thread
  2. Preglasite konstruktor __init__
  3. Preglasite metodo run ()

Ko je predmet niti narejen, lahko z metodo start () začnete izvajanje te dejavnosti, z metodo join () pa blokirate vso drugo kodo, dokler se trenutna dejavnost ne konča.

Zdaj pa poskusimo uporabiti modul za navoje za izvajanje vašega prejšnjega primera. Še enkrat zaženite svoj IDLE in vnesite naslednje:

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()

To bo rezultat, ko zaženete zgornjo kodo:

POJASNILO KODE

  1. Ta del je enak našemu prejšnjemu primeru. Tukaj uvozite modul časa in niti, ki se uporabljata za obdelavo izvajanja in zamud niti Python.
  2. V tem bitu ustvarjate razred, imenovan threadtester, ki podeduje ali razširi razred Thread modula za navoje. To je eden najpogostejših načinov ustvarjanja niti v pythonu. Vendar bi morali v svoji aplikaciji preglasiti samo konstruktor in metodo run () . Kot lahko vidite v zgornjem vzorcu kode, je bila metoda __init__ (konstruktor) razveljavljena.

    Podobno ste preglasili tudi metodo run () . Vsebuje kodo, ki jo želite izvršiti znotraj niti. V tem primeru ste poklicali funkcijo thread_test ().

  3. To je metoda thread_test (), ki za argument vzame vrednost i , jo na vsaki ponovitvi zmanjša za 1 in se skozi ostalo kodo vrti, dokler i ne postane 0. V vsaki ponovitvi natisne ime trenutno izvajane niti in spi nekaj sekund čakanja (kar je tudi vzeto kot argument).
  4. thread1 = threadtester (1, "Prva nit", 1)

    Tu ustvarjamo nit in posredujemo tri parametre, ki smo jih deklarirali v __init__. Prvi parameter je id niti, drugi parameter je ime niti, tretji parameter pa je števec, ki določa, kolikokrat naj se zažene zanka while.

  5. thread2.start ()

    Začetna metoda se uporablja za začetek izvajanja niti. Funkcija start () interno pokliče metodo run () vašega razreda.

  6. thread3.join ()

    Metoda join () blokira izvajanje druge kode in počaka, dokler se nit, na kateri je bila imenovana, ne konča.

Kot že veste, imajo niti, ki so v istem procesu, dostop do pomnilnika in podatkov tega procesa. Kot rezultat, če več kot ena nit poskuša hkrati spremeniti ali dostopati do podatkov, lahko pride do napak.

V naslednjem razdelku boste videli različne vrste zapletov, ki se lahko pojavijo, ko niti dostopajo do podatkov in kritičnega odseka brez preverjanja obstoječih transakcij dostopa.

Zastoji in dirkalni pogoji

Preden se naučimo zastojev in dirkalnih razmer, je koristno razumeti nekaj osnovnih definicij, povezanih s sočasnim programiranjem:

  • Kritični oddelek

    To je fragment kode, ki dostopa do spremenljivk v skupni rabi ali jih spreminja in mora biti izveden kot atomska transakcija.

  • Kontekstno stikalo

    To je postopek, ki ga sledi CPU za shranjevanje stanja niti, preden se preklopi z ene naloge na drugo, tako da jo je mogoče kasneje nadaljevati z iste točke.

Zastoji

Zamude so najbolj zaskrbljena težava, s katero se srečujejo razvijalci pri pisanju sočasnih / večnitnih aplikacij v python. Najboljši način za razumevanje zastojev je uporaba klasičnega primera računalništva, znanega kot problem jedilnih filozofov.

Izjava problema za jedilne filozofe je naslednja:

Pet filozofov sedi na okrogli mizi s petimi krožniki špagetov (vrsta testenin) in petimi vilicami, kot je prikazano na diagramu.

Problem jedilnih filozofov

V določenem trenutku mora filozof ali jesti ali razmišljati.

Poleg tega mora filozof vzeti dve sosednji vilici (tj. Levo in desno vilice), preden lahko poje špagete. Problem zastoja se pojavi, ko vseh pet filozofov istočasno vzame desne vilice.

Ker ima vsak od filozofov eno vilico, bodo vsi počakali, da bodo ostali položili vilice. Posledično nobeden od njih ne bo mogel jesti špagete.

Podobno v sočasnem sistemu pride do zastoja, ko različne niti ali procesi (filozofi) poskušajo hkrati pridobiti skupne sistemske vire (vilice). Posledično nobeden od procesov nima možnosti za izvedbo, saj čaka na drug vir, ki ga ima nek drug postopek.

Dirkalni pogoji

Dirkalni pogoj je neželeno stanje programa, ki se pojavi, ko sistem izvede dve ali več operacij hkrati. Na primer, upoštevajte to preprosto for zanko:

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

Če ustvarite n števila niti, ki hkrati izvajajo to kodo, ne morete določiti vrednosti i (ki je v skupni rabi niti), ko program zaključi izvajanje. To je zato, ker se lahko v resničnem okolju z več nitmi niti prekrivajo, vrednost i, ki jo je nit privzela in spremenila, pa se lahko spremeni, ko neka druga nit dostopa do nje.

To sta dva glavna razreda težav, ki se lahko pojavijo v večnitni ali porazdeljeni aplikaciji python. V naslednjem razdelku boste izvedeli, kako s sinhronizacijo niti premagati to težavo.

Sinhronizacija niti

Za reševanje dirkalnih pogojev, zastojev in drugih težav, ki temeljijo na nitkah, modul za navoja ponuja objekt Lock . Ideja je, da ko nit želi dostop do določenega vira, pridobi ključavnico za ta vir. Ko nit zaklene določen vir, nobena druga nit ne more dostopati do njega, dokler se ključavnica ne sprosti. Posledično bodo spremembe vira atomske in dirkalne razmere bodo preprečene.

Ključavnica je primitiv za sinhronizacijo na nizki ravni, ki ga izvaja modul __thread . Ključavnica je lahko kadar koli v enem od dveh stanj: zaklenjena ali odklenjena. Podpira dve metodi:

  1. pridobiti ()

    Ko je stanje zaklepanja odklenjeno, bo klic metode accept () spremenil stanje v zaklenjeno in se vrnil. Če pa je stanje zaklenjeno, je klic za pridobitev () blokiran, dokler druga nit ne pokliče metode release ().

  2. sprostitev ()

    Metoda release () se uporablja za nastavitev stanja na odklenjeno, tj. Za sprostitev zaklepanja. Pokliče ga lahko katera koli nit, ni nujno tista, ki je pridobila ključavnico.

Tu je primer uporabe zaklepanja v vaših aplikacijah. Zaženite svoj IDLE in vnesite naslednje:

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()

Zdaj pritisnite F5. Morali bi videti takšen izhod:

POJASNILO KODE

  1. Tu preprosto ustvarite novo ključavnico s klicem tovarniške funkcije threadading.Lock () . Lock () vrne primerek najučinkovitejšega konkretnega razreda Lock, ki ga vzdržuje platforma.
  2. V prvem stavku zaklepanje pridobite s klicem metode pridobivanja (). Ko je zaklepanje odobreno, na konzolo natisnete "pridobljeno zaklepanje" . Ko je vsa koda, za katero želite, da se nit izvaja, končana, sprostite zaklepanje s klicem metode release ().

Teorija je v redu, toda kako veste, da je ključavnica res delovala? Če pogledate izhodne podatke, boste videli, da vsaka izjava za tiskanje tiska natanko po eno vrstico naenkrat. Spomnimo se, da so bili v prejšnjem primeru izhodi iz tiskanja naključni, ker je več niti hkrati dostopalo do metode print (). Tu se funkcija tiskanja pokliče šele po pridobitvi ključavnice. Torej, izhodi se prikažejo posamezno in vrstica za vrstico.

Poleg ključavnic python podpira tudi nekatere druge mehanizme za obdelavo sinhronizacije niti, kot je navedeno spodaj:

  1. RLocks
  2. Semaforji
  3. Pogoji
  4. Dogodki in
  5. Ovire

Global Interpreter Lock (in kako ravnati z njim)

Preden se poglobimo v podrobnosti pythonovega GIL, določimo nekaj izrazov, ki bodo koristni za razumevanje prihajajočega razdelka:

  1. Koda, vezana na CPU: to se nanaša na kateri koli del kode, ki ga bo CPU neposredno izvedel.
  2. I / O vezana koda: to je lahko katera koli koda, ki dostopa do datotečnega sistema prek OS
  3. CPython: je referenčna izvedba Pythona in jo lahko opišemo kot tolmač, napisan v C in Python (programskem jeziku).

Kaj je GIL v Pythonu?

Global Interpreter Lock (GIL) v pythonu je zaklepanje procesa ali mutex, ki se uporablja pri obravnavi procesov. Zagotavlja, da lahko ena nit hkrati dostopa do določenega vira, hkrati pa preprečuje uporabo predmetov in bajt kod hkrati. To koristi enonitnim programom pri povečanju zmogljivosti. GIL v pythonu je zelo preprost in enostaven za izvedbo.

S ključavnico lahko zagotovite, da ima dostop do določenega vira v določenem trenutku samo ena nit.

Ena od značilnosti Pythona je, da uporablja globalno zaklepanje vsakega procesa tolmača, kar pomeni, da vsak proces interpretira samega python kot vir.

Denimo, da ste napisali program python, ki uporablja dve niti za izvajanje CPU in 'I / O' operacij. Ko zaženete ta program, se zgodi naslednje:

  1. Tolmač python ustvari nov postopek in ustvari niti
  2. Ko se nit-1 začne izvajati, najprej pridobi GIL in jo zaklene.
  3. Če želi nit-2 izvršiti zdaj, bo moral počakati, da se sprosti GIL, tudi če je drug procesor brezplačen.
  4. Recimo, da nit-1 čaka na operacijo V / I. Trenutno bo sprostil GIL in nit-2 ga bo pridobil.
  5. Po dokončanju operacij V / I, če hoče nit-1 izvajati zdaj, bo spet moral počakati, da bo GIL sprosti nit-2.

Zaradi tega lahko do tolmača kadar koli dostopa samo ena nit, kar pomeni, da bo v določenem trenutku samo ena nit izvajala kodo pythona.

To je v redu z enojedrnim procesorjem, ker bi za obdelavo niti uporabljal časovno rezanje (glej prvi odsek te vadnice). Vendar pa bo v primeru večjedrnih procesorjev funkcija, vezana na CPU, ki se izvaja na več nitih, močno vplivala na učinkovitost programa, saj dejansko ne bo uporabljal vseh razpoložljivih jeder hkrati.

Zakaj je bil potreben GIL?

Zbiralnik smeti CPython uporablja učinkovito tehniko upravljanja pomnilnika, znano kot štetje referenc. Tako deluje: Vsak objekt v pythonu ima število sklicev, ki se poveča, ko je dodeljeno novemu imenu spremenljivke ali dodano v vsebnik (kot so nabori, seznami itd.) Prav tako se število sklicev zmanjša, ko sklic izstopi iz obsega ali ko se pokliče stavek del. Ko referenčno število predmeta doseže 0, se zbere smeti in dodeljeni pomnilnik se sprosti.

Toda težava je v tem, da je spremenljivka števila referenc nagnjena k dirkalnim pogojem kot katera koli druga globalna spremenljivka. Da bi rešili to težavo, so se razvijalci pythona odločili za globalno zaklepanje tolmačev. Druga možnost je bila, da vsakemu objektu dodate ključavnico, kar bi povzročilo blokade in povečalo režijske stroške zaradi klicev pridobivanja () in sprostitve ().

Zato je GIL pomembna omejitev za večnitne programe python, ki izvajajo težke operacije, vezane na CPU (zaradi česar so enonitni). Če želite v svoji aplikaciji uporabiti več jeder CPU, raje uporabite večprocesorski modul.

Povzetek

  • Python podpira 2 modula za večnitnost:
    1. __thread modul: Zagotavlja nizko raven izvedbe za navoja in je zastarel.
    2. Navojni modul : Zagotavlja visoko raven izvedbe za večnitnost in je trenutni standard.
  • Če želite z nitnim modulom ustvariti nit, morate storiti naslednje:
    1. Ustvarite razred, ki razširja razred Thread .
    2. Preglasite njegov konstruktor (__init__).
    3. Preglasite njegovo metodo run () .
    4. Ustvari objekt tega razreda.
  • Nit lahko izvedemo s klicem metode start () .
  • Metoda join () se lahko uporablja za blokiranje drugih niti, dokler ta nit (tista, na kateri je bilo poklicano združevanje) ne zaključi izvajanja.
  • Pogoj dirke se pojavi, ko več niti hkrati dostopa ali spreminja vir v skupni rabi.
  • Temu se lahko izognete s sinhronizacijo niti.
  • Python podpira 6 načinov sinhronizacije niti:
    1. Ključavnice
    2. RLocks
    3. Semaforji
    4. Pogoji
    5. Dogodki in
    6. Ovire
  • Ključavnice dovolijo vstop v kritični del samo določeni niti, ki je dobila ključavnico.
  • Ključavnica ima 2 glavni metodi:
    1. pridobiti () : stanje zaklepanja nastavi na zaklenjeno. Če pokličete zaklenjeni predmet, se blokira, dokler vir ni prost.
    2. release () : Nastavi zaklenjeno stanje na odklenjeno in se vrne. Če pokličete odklenjen predmet, vrne false.
  • Zaklepanje globalnega tolmača je mehanizem, s pomočjo katerega lahko hkrati izvede samo 1 proces tolmačenja CPython.
  • Uporabil se je za lažjo funkcijo štetja referenc zbiralnika smeti CPythons.
  • Če želite izdelovati aplikacije Python s težkimi operacijami, povezanimi s procesorjem, uporabite modul za večprocesorsko obdelavo.