Multithreading în Python cu Exemplu Global Interpreter Lock (GIL)

Cuprins:

Anonim

Limbajul de programare python vă permite să utilizați multiprocesare sau multithreading. În acest tutorial, veți învăța cum să scrieți aplicații multithreaded în Python.

Ce este un fir?

Un fir de execuție este o unitate de execție în programarea simultană. Multithreading este o tehnică care permite unui CPU să execute mai multe sarcini ale unui proces în același timp. Aceste fire se pot executa individual în timp ce își partajează resursele de proces.

Ce este un proces?

Un proces este practic programul în execuție. Când porniți o aplicație în computer (cum ar fi un browser sau un editor de text), sistemul de operare creează un proces.

Ce este Multithreading în Python?

Multithreading în programarea Python este o tehnică binecunoscută în care mai multe fire dintr-un proces își partajează spațiul de date cu firul principal, ceea ce face schimbul de informații și comunicarea în cadrul threadurilor ușor și eficient. Firele sunt mai ușoare decât procesele. Multi fire pot fi executate individual în timp ce își partajează resursele de proces. Scopul multithreading-ului este de a rula mai multe sarcini și celule funcționale în același timp.

Ce este multiprocesarea?

Multiprocesarea vă permite să rulați simultan mai multe procese fără legătură. Aceste procese nu își împărtășesc resursele și comunică prin IPC.

Python Multithreading vs Multiprocessing

Pentru a înțelege procesele și firele, luați în considerare acest scenariu: Un fișier .exe de pe computer este un program. Când îl deschideți, sistemul de operare îl încarcă în memorie și procesorul îl execută. Instanța programului care rulează acum se numește proces.

Fiecare proces va avea 2 componente fundamentale:

  • Codul
  • Datele

Acum, un proces poate conține una sau mai multe sub-părți numite fire. Acest lucru depinde de arhitectura sistemului de operare. Vă puteți gândi la un fir ca o secțiune a procesului care poate fi executată separat de sistemul de operare.

Cu alte cuvinte, este un flux de instrucțiuni care poate fi rulat independent de sistemul de operare. Firele dintr-un singur proces împărtășesc datele acelui proces și sunt proiectate să funcționeze împreună pentru a facilita paralelismul.

În acest tutorial, veți învăța,

  • Ce este un fir?
  • Ce este un proces?
  • Ce este Multithreading?
  • Ce este multiprocesarea?
  • Python Multithreading vs Multiprocessing
  • De ce să folosiți Multithreading?
  • Python MultiThreading
  • Modulele Thread și Threading
  • Modulul Thread
  • Modulul de filetare
  • Blocaje și condiții de cursă
  • Sincronizarea firelor
  • Ce este GIL?
  • De ce a fost nevoie de GIL?

De ce să folosiți Multithreading?

Multithreading vă permite să împărțiți o aplicație în mai multe sub-sarcini și să executați aceste sarcini simultan. Dacă utilizați corect multi-threading, viteza aplicației, performanța și redarea pot fi îmbunătățite.

Python MultiThreading

Python acceptă construcții atât pentru multiprocesare, cât și pentru multiprocesare. În acest tutorial, vă veți concentra în primul rând pe implementarea aplicațiilor cu mai multe fire cu python. Există două module principale care pot fi utilizate pentru a gestiona fire în Python:

  1. Firului modulului, și
  2. Filetat Modulul

Cu toate acestea, în Python, există și ceva numit blocare globală pentru interpret (GIL). Nu permite mult câștig de performanță și poate chiar reduce performanța unor aplicații cu mai multe fire. Veți afla totul despre asta în următoarele secțiuni ale acestui tutorial.

Modulele Thread și Threading

Cele două module despre care veți afla în acest tutorial sunt modulul thread și modulul threading .

Cu toate acestea, modulul thread a fost depreciat de mult. Începând cu Python 3, a fost desemnat ca fiind învechit și este accesibil doar ca __thread pentru compatibilitate inversă.

Ar trebui să utilizați modulul de filetare de nivel superior pentru aplicațiile pe care intenționați să le implementați. Modulul thread a fost acoperit aici doar în scopuri educaționale.

Modulul Thread

Sintaxa pentru a crea un nou thread utilizând acest modul este următoarea:

thread.start_new_thread(function_name, arguments)

Bine, acum ați acoperit teoria de bază pentru a începe codarea. Deci, deschideți IDLE-ul sau un blocnotes și introduceți următoarele:

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

Salvați fișierul și apăsați F5 pentru a rula programul. Dacă totul a fost făcut corect, acesta este rezultatul pe care ar trebui să-l vedeți:

Veți afla mai multe despre condițiile cursei și despre cum să le gestionați în următoarele secțiuni

EXPLICAȚIE COD

  1. Aceste declarații importă modulul de timp și fir care sunt utilizate pentru a gestiona execuția și întârzierea firelor Python.
  2. Aici, ați definit o funcție numită thread_test, care va fi apelată prin metoda start_new_thread . Funcția rulează o buclă de timp pentru patru iterații și imprimă numele firului care l-a numit. Odată ce iterația este completă, imprimă un mesaj care spune că firul a terminat execuția.
  3. Aceasta este secțiunea principală a programului dvs. Aici, pur și simplu apelați metoda start_new_thread cu funcția thread_test ca argument.

    Aceasta va crea un nou fir pentru funcția pe care o treceți ca argument și va începe să o executați. Rețineți că puteți înlocui acest (thread _ test) cu orice altă funcție pe care doriți să o rulați ca thread.

Modulul de filetare

Acest modul este implementarea la nivel înalt a filetării în python și standardul de facto pentru gestionarea aplicațiilor multithread. Oferă o gamă largă de caracteristici în comparație cu modulul thread.

Structura modulului de filetare

Iată o listă cu câteva funcții utile definite în acest modul:

Numele funcției Descriere
activeCount () Returnează numărul de obiecte Thread care sunt încă în viață
currentThread () Returnează obiectul curent al clasei Thread.
enumera() Listează toate obiectele Thread active.
isDaemon () Returnează adevărat dacă firul este un daemon.
este in viata() Returnează adevărat dacă firul este încă viu.
Metode de clasă Thread
start() Începe activitatea unui thread. Trebuie să fie apelat o singură dată pentru fiecare fir, deoarece va genera o eroare de rulare dacă este apelat de mai multe ori.
alerga() Această metodă denotă activitatea unui thread și poate fi anulată de o clasă care extinde clasa Thread.
a te alatura() Blochează execuția altor coduri până când firul pe care a fost apelată metoda join () este terminat.

Backstory: The Thread Class

Înainte de a începe să codați programe multithread folosind modulul de threading, este crucial să înțelegeți despre clasa Thread. Clasa thread este clasa principală care definește șablonul și operațiunile unui thread în python.

Cel mai comun mod de a crea o aplicație Python multithread este să declarați o clasă care extinde clasa Thread și suprascrie metoda run ().

Clasa Thread, în rezumat, semnifică o secvență de cod care rulează într-un fir separat de control.

Deci, atunci când scrieți o aplicație cu mai multe fire, veți face următoarele:

  1. definiți o clasă care extinde clasa Thread
  2. Înlocuiți constructorul __init__
  3. Suprascria alerga) ( metoda

Odată ce un obiect thread a fost realizat, metoda start () poate fi utilizată pentru a începe executarea acestei activități și metoda join () poate fi utilizată pentru a bloca toate celelalte coduri până când activitatea curentă se termină.

Acum, să încercăm să folosim modulul de filetare pentru a implementa exemplul anterior. Din nou, aprindeți IDLE și introduceți următoarele:

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

Aceasta va fi ieșirea atunci când executați codul de mai sus:

EXPLICAȚIE COD

  1. Această parte este aceeași cu exemplul nostru anterior. Aici, importați modulul de timp și fir care sunt utilizate pentru a gestiona execuția și întârzierile firelor Python.
  2. În acest bit, creați o clasă numită threadtester, care moștenește sau extinde clasa Thread a modulului de threading. Acesta este unul dintre cele mai comune moduri de a crea fire în python. Cu toate acestea, ar trebui să suprascrieți constructorul și metoda run () din aplicația dvs. După cum puteți vedea în exemplul de cod de mai sus, metoda __init__ (constructor) a fost anulată.

    În mod similar, ați suprascris și metoda run () . Acesta conține codul pe care doriți să-l executați în interiorul unui thread. În acest exemplu, ați numit funcția thread_test ().

  3. Aceasta este metoda thread_test () care ia valoarea i ca argument, o scade cu 1 la fiecare iterație și parcurge restul codului până când i devine 0. În fiecare iterație, imprimă numele firului executant curent și doarme câteva secunde de așteptare (care este, de asemenea, luat ca argument).
  4. thread1 = threadtester (1, „Primul fir”, 1)

    Aici, creăm un fir și trecem cei trei parametri pe care i-am declarat în __init__. Primul parametru este id-ul firului, al doilea parametru este numele firului, iar al treilea parametru este contorul, care determină de câte ori ar trebui să ruleze bucla while.

  5. thread2.start ()

    Metoda de pornire este utilizată pentru a începe executarea unui thread. Intern, funcția start () apelează metoda run () a clasei dvs.

  6. thread3.join ()

    Metoda join () blochează execuția altor coduri și așteaptă până când se termină firul pe care a fost numit.

După cum știți deja, firele care se află în același proces au acces la memoria și datele acelui proces. Ca rezultat, dacă mai multe fire încearcă să modifice sau să acceseze datele simultan, se pot strecura erori.

În secțiunea următoare, veți vedea diferitele tipuri de complicații care pot apărea atunci când firele accesează datele și secțiunea critică, fără a verifica tranzacțiile de acces existente.

Blocaje și condiții de cursă

Înainte de a afla despre blocaje și condițiile cursei, va fi util să înțelegeți câteva definiții de bază legate de programarea concurentă:

  • Secțiunea critică

    Este un fragment de cod care accesează sau modifică variabilele partajate și trebuie efectuat ca o tranzacție atomică.

  • Comutator context

    Este procesul pe care îl urmează un CPU pentru a stoca starea unui thread înainte de a trece de la o sarcină la alta, astfel încât să poată fi reluat din același punct mai târziu.

Blocaje

Blocajele sunt cea mai temută problemă cu care se confruntă dezvoltatorii atunci când scriu aplicații simultane / multithreaded în python. Cea mai bună modalitate de a înțelege blocajele este folosind exemplul clasic al problemei de informatică cunoscut sub numele de Dining Philosophers Problem.

Declarația problemei pentru filosofii culinare este următoarea:

Cinci filozofi sunt așezați pe o masă rotundă cu cinci farfurii de spaghete (un tip de paste) și cinci furculițe, așa cum se arată în diagramă.

Problema filozofilor de luat masa

În orice moment, un filosof trebuie să mănânce sau să gândească.

Mai mult, un filozof trebuie să ia cele două furci adiacente (adică furcile din stânga și din dreapta) înainte de a putea mânca spaghetele. Problema blocajului apare atunci când toți cei cinci filosofi își ridică furcile potrivite simultan.

Deoarece fiecare dintre filozofi are o singură furculiță, toți vor aștepta ca ceilalți să pună furca jos. Drept urmare, niciunul dintre ei nu va putea mânca spaghete.

În mod similar, într-un sistem concurent, un blocaj apare atunci când diferite fire sau procese (filosofi) încearcă să dobândească resursele de sistem partajate (furculițe) în același timp. Ca urmare, niciunul dintre procese nu are șansa de a se executa, deoarece așteaptă o altă resursă deținută de un alt proces.

Condiții de cursă

O condiție de cursă este o stare nedorită a unui program care apare atunci când un sistem efectuează două sau mai multe operațiuni simultan. De exemplu, luați în considerare acest lucru simplu pentru buclă:

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

Dacă creați n număr de fire care rulează acest cod simultan, nu puteți determina valoarea lui i (care este partajată de fire) atunci când programul termină execuția. Acest lucru se datorează faptului că într-un mediu real multi-threading, firele se pot suprapune, iar valoarea lui i care a fost recuperată și modificată de un fir se poate schimba între ele când un alt fir îl accesează.

Acestea sunt cele două clase principale de probleme care pot apărea într-o aplicație Python multithread sau distribuită. În secțiunea următoare, veți afla cum să depășiți această problemă sincronizând fire.

Sincronizarea firelor

Pentru a rezolva condițiile cursei, blocajele și alte probleme bazate pe fire, modulul de filetare oferă obiectul Lock . Ideea este că atunci când un fir dorește accesul la o anumită resursă, acesta dobândește un blocaj pentru resursa respectivă. Odată ce un fir blocează o anumită resursă, niciun alt fir nu îl poate accesa până când blocarea nu este eliberată. Ca urmare, modificările resursei vor fi atomice, iar condițiile rasei vor fi evitate.

O blocare este o primitivă de sincronizare de nivel scăzut implementată de modulul __thread . În orice moment, o blocare poate fi în una din cele 2 stări: blocată sau deblocată. Suportă două metode:

  1. dobândi()

    Când starea de blocare este deblocată, apelarea metodei acquire () va schimba starea în blocat și va reveni. Cu toate acestea, dacă starea este blocată, apelul de a dobândi () este blocat până când metoda release () este apelată de un alt thread.

  2. eliberare()

    Metoda release () este utilizată pentru a seta starea la deblocat, adică pentru a elibera o blocare. Poate fi apelat de orice fir, nu neapărat de cel care a dobândit încuietoarea.

Iată un exemplu de utilizare a blocărilor în aplicațiile dvs. Aprindeți IDLE și tastați următoarele:

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

Acum, apasă pe F5. Ar trebui să vedeți o ieșire ca aceasta:

EXPLICAȚIE COD

  1. Aici, pur și simplu creați o nouă blocare apelând funcția de fabrică threading.Lock () . Intern, Lock () returnează o instanță a celei mai eficiente clase de blocare a betonului, care este întreținută de platformă.
  2. În prima declarație, obțineți blocarea apelând metoda acquise (). Când blocarea a fost acordată, tipăriți „blocarea dobândită” pe consolă. Odată ce tot codul pe care doriți să-l ruleze a terminat execuția, eliberați blocarea apelând metoda release ().

Teoria este în regulă, dar de unde știi că încuietoarea a funcționat cu adevărat? Dacă vă uitați la ieșire, veți vedea că fiecare dintre declarațiile de tipărire tipărește exact un rând la rând. Amintiți-vă că, într-un exemplu anterior, ieșirile din tipărire au fost întâmplătoare, deoarece mai multe fire au accesat metoda print () în același timp. Aici, funcția de imprimare este apelată numai după ce a fost achiziționată blocarea. Deci, ieșirile sunt afișate pe rând și rând cu rând.

În afară de blocări, python acceptă și alte mecanisme pentru gestionarea sincronizării firelor, așa cum este listat mai jos:

  1. RLocks
  2. Semaforele
  3. Condiții
  4. Evenimente și
  5. Bariere

Global Interpreter Lock (și cum să îl rezolvați)

Înainte de a intra în detaliile GIL-ului python, să definim câțiva termeni care vor fi utili în înțelegerea următoarei secțiuni:

  1. Cod legat de CPU: se referă la orice bucată de cod care va fi executată direct de CPU.
  2. Cod legat de I / O: acesta poate fi orice cod care accesează sistemul de fișiere prin sistemul de operare
  3. CPython: este implementarea de referință a Python și poate fi descris ca interpretul scris în C și Python (limbaj de programare).

Ce este GIL în Python?

Global Interpreter Lock (GIL) în python este un blocaj de proces sau un mutex utilizat în timpul procesului. Se asigură că un fir poate accesa o anumită resursă la un moment dat și, de asemenea, împiedică utilizarea obiectelor și codurilor secundare simultan. Acest lucru aduce beneficii programelor cu fir unic într-o creștere a performanței. GIL în python este foarte simplu și ușor de implementat.

O blocare poate fi utilizată pentru a vă asigura că doar un fir are acces la o anumită resursă la un moment dat.

Una dintre caracteristicile Python este că folosește un blocaj global la fiecare proces de interpretare, ceea ce înseamnă că fiecare proces tratează interpretul python însuși ca o resursă.

De exemplu, să presupunem că ați scris un program python care folosește două fire pentru a efectua atât operații CPU cât și operații „I / O”. Când executați acest program, așa se întâmplă:

  1. Interpretul Python creează un nou proces și generează firele
  2. Când firul-1 începe să ruleze, acesta va achiziționa mai întâi GIL și îl va bloca.
  3. Dacă thread-2 vrea să se execute acum, va trebui să aștepte lansarea GIL chiar dacă un alt procesor este liber.
  4. Acum, să presupunem că thread-1 așteaptă o operație de I / O. În acest moment, va elibera GIL, iar thread-2 îl va achiziționa.
  5. După finalizarea opțiunilor I / O, dacă thread-1 vrea să se execute acum, va trebui să aștepte din nou ca GIL să fie lansat de thread-2.

Datorită acestui fapt, un singur fir poate accesa interpretul în orice moment, ceea ce înseamnă că va exista un singur fir care execută codul Python la un moment dat de timp.

Acest lucru este în regulă într-un procesor cu un singur nucleu, deoarece ar folosi timp de tranșare (a se vedea prima secțiune a acestui tutorial) pentru a gestiona firele. Cu toate acestea, în cazul procesoarelor multi-core, o funcție legată de CPU care se execută pe mai multe fire de execuție va avea un impact considerabil asupra eficienței programului, deoarece nu va utiliza de fapt toate nucleele disponibile în același timp.

De ce a fost nevoie de GIL?

Colectorul de gunoi CPython folosește o tehnică eficientă de gestionare a memoriei cunoscută sub numele de numărare a referințelor. Iată cum funcționează: Fiecare obiect din python are un număr de referințe, care este mărit atunci când este atribuit unui nou nume de variabilă sau adăugat la un container (cum ar fi tupluri, liste etc.). În mod similar, numărul de referințe este redus atunci când referința iese din sfera de aplicare sau când este apelată instrucțiunea del. Când numărul de referință al unui obiect ajunge la 0, acesta este colectat gunoi, iar memoria alocată este eliberată.

Dar problema este că variabila numărului de referință este predispusă la condiții de rasă, ca orice altă variabilă globală. Pentru a rezolva această problemă, dezvoltatorii de python au decis să folosească blocarea interpretorului global. Cealaltă opțiune a fost să adăugați o blocare la fiecare obiect, ceea ce ar fi dus la blocaje și la creșterea cheltuielilor generale de la apelurile de achiziție () și eliberare ().

Prin urmare, GIL este o restricție semnificativă pentru programele python multithread care rulează operațiuni grele legate de CPU (făcându-le efectiv single-threaded). Dacă doriți să utilizați mai multe nuclee CPU în aplicația dvs., utilizați în schimb modulul multiprocesare .

rezumat

  • Python acceptă 2 module pentru multithreading:
    1. __modul thread : oferă o implementare de nivel scăzut pentru threading și este învechit.
    2. modul de filetare : oferă o implementare la nivel înalt pentru multithreading și este standardul actual.
  • Pentru a crea un thread utilizând modulul de threading, trebuie să faceți următoarele:
    1. Creați o clasă care extinde clasa Thread .
    2. Înlocuiți constructorul său (__init__).
    3. Anulați metoda run () .
    4. Creați un obiect din această clasă.
  • Un fir poate fi executat apelând metoda start () .
  • Metoda join () poate fi utilizată pentru a bloca alte fire până când acest thread (cel pe care s-a apelat join) termină execuția.
  • O condiție de cursă apare atunci când mai multe fire de acces accesează sau modifică o resursă partajată în același timp.
  • Poate fi evitat prin sincronizarea firelor.
  • Python acceptă 6 moduri de sincronizare a firelor:
    1. Încuietori
    2. RLocks
    3. Semaforele
    4. Condiții
    5. Evenimente și
    6. Bariere
  • Încuietorile permit doar un anumit fir care a dobândit încuietoarea să intre în secțiunea critică.
  • O blocare are 2 metode principale:
    1. acquire () : Setează starea de blocare la blocat. Dacă este apelat la un obiect blocat, acesta se blochează până când resursa este liberă.
    2. release () : Setează starea de blocare la deblocat și revine. Dacă este apelat la un obiect deblocat, acesta returnează fals.
  • Blocarea globală a interpretului este un mecanism prin care se poate executa o singură dată un proces de interpretare CPython.
  • A fost folosit pentru a facilita funcționalitatea de numărare a referințelor pentru colectorul de gunoi CPythons.
  • Pentru a crea aplicații Python cu operațiuni grele legate de CPU, ar trebui să utilizați modulul multiprocesare.