Multithreading în Python cu Exemplu: Învață GIL în Python

⚡ Rezumat inteligent

Multithreading în Python Rulează mai multe fire de execuție în cadrul unui singur proces, astfel încât acestea să partajeze memoria și să lucreze concomitent. Modulul de threading creează și gestionează aceste fire de execuție, în timp ce Global Interpreter Lock limitează paralelismul real, făcând tehnica cea mai potrivită pentru sarcinile legate de intrare/ieșire.

  • 🔘 Fir de acțiune vs. proces: Un fir de execuție este o unitate de execuție în interiorul unui proces, iar firele de execuție partajează datele procesului în timp ce rulează individual.
  • ☑️ Două module: Modulul învechit _thread oferă apeluri de nivel scăzut, în timp ce modulul de threading de nivel superior este standardul modern pentru construirea de thread-uri.
  • Creați fire de discuție: Extindeți clasa Thread, suprascrieți __init__ și run(), apoi apelați start() pentru lansare și join() pentru a aștepta finalizarea.
  • 🧪 Evitați condițiile de cursă: Un blocaj permite unui singur fir de execuție să intre într-o secțiune critică la un moment dat, prevenind blocajele și coruperea datelor partajate.
  • 🛠️ Înțelegeți GIL-ul: Blocarea interpretorului global permite rularea unui fir de execuție Python bytecode odată, deci munca legată de CPU necesită multiprocesare.
  • 🤖 Sarcini de lucru AI: Multithreading-ul accelerează pașii AI legați de intrare/ieșire, cum ar fi încărcarea datelor, în timp ce NumPy și PyTorch eliberează GIL-ul pentru calcul paralel.

Multithreading în Python

Python limbajul de programare 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 Thread?

Un fir de execuție (thread) este o unitate de execuție în programarea concurentă. Multithreading-ul este o tehnică ce permite unui procesor (CPU) să execute mai multe sarcini ale unui proces în același timp. Aceste fire de execuție se pot executa individual, partajând în același timp resursele procesului.

Ce este un proces?

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

În ce este Multithreadingul Python?

Multithreading în Python Programarea este o tehnică binecunoscută în care mai multe fire de execuție dintr-un proces își partajează spațiul de date cu firul principal, ceea ce face ca partajarea informațiilor și comunicarea în cadrul firelor de execuție să fie ușoară și eficientă. Firele de execuție sunt mai ușoare decât procesele. Mai multe fire de execuție se pot executa individual, partajând în același timp resursele procesului. Scopul multithreading-ului este de a rula mai multe sarcini și funcții în același timp.

Ce este Multiprocesarea?

Multiprocesare vă permite să rulați simultan mai multe procese care nu au legătură. Aceste procese nu își împart resursele și comunică prin IPC.

Python Multithreading vs Multiprocesare

Pentru a înțelege procesele și firele de execuție, 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, iar procesorul îl execută. Instanța programului care rulează acum se numește proces.

Fiecare proces are două componente fundamentale:

  • Code
  • 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 de execuție ca la o secțiune a procesului care poate fi executată separat de sistemul de operare.

Cu alte cuvinte, este un flux de instrucțiuni care pot fi rulate independent de către sistemul de operare. Firele de execuție dintr-un singur proces partajează datele acelui proces și sunt proiectate să lucreze împreună pentru a facilita paralelismul.

De ce să folosiți Multithreading?

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

Python multithreading

Python suportă construcții atât pentru multiprocesare, cât și pentru multithreading. În acest tutorial, vă veți concentra în principal pe implementarea multithread aplicatii cu PythonExistă două module principale care pot fi utilizate pentru a gestiona firele de execuție în Python:

  1. fir modul, și
  2. filetat modul

Cu toate acestea, în Python, există și ceva numit blocare globală a interpretorului (GIL). Nu permite un câștig semnificativ de performanță și poate chiar reduce performanța unor aplicații multithreaded. Veți afla totul despre el în secțiunile următoare ale acestui tutorial.

Modulele Thread și Threading

Cele două module despre care veți afla în acest tutorial sunt modul de filet si modul de filetare.

Cu toate acestea, modulul thread a fost demult depreciat. Incepand cu Python 3, a fost desemnat ca învechit și este accesibil doar ca fir de acțiune pentru compatibilitate inversă.

Ar trebui să utilizați nivelul superior filetat modul pentru aplicațiile pe care intenționați să le implementați. Modulul thread a fost prezentat aici doar în scopuri educaționale.

Modulul Thread

Sintaxa pentru a crea un fir nou folosind 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 sau un bloc de note și tastați următoarele:

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

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

Modulul Thread

Vei afla mai multe despre condițiile de cursă și cum să le gestionezi în secțiunile următoare.

Modulul Thread

EXPLICAȚIA CODULUI

  1. Aceste instrucțiuni importă modulul time și thread, care sunt folosite pentru a gestiona execuția și întârzierea Python fire.
  2. Aici, ați definit o funcție numită thread_test, care va fi numit de către start_new_thread metodă. Funcția execută o buclă while timp de patru iterații și afișează numele firului de execuție care a apelat-o. Odată ce iterația este completă, afișează un mesaj care spune că firul de execuție a terminat.
  3. Aceasta este secțiunea principală a programului dvs. Aici, pur și simplu sunați la start_new_thread metoda cu thread_test funcția ca argument. Aceasta va crea un nou fir de execuție pentru funcția pe care o transmiteți ca argument și va începe execuția acestuia. Rețineți că puteți înlocui acest (thread_test) cu orice altă funcție pe care doriți să o rulați ca fir de execuție.

Modulul Threading

Acest modul este implementarea la nivel înalt a threading-ului Python și standardul de facto pentru gestionarea aplicațiilor multithreaded. Oferă o gamă largă de caracteristici în comparație cu modulul filet.

Structura modulului Threading

Structura modulului Threading

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

Numele funcției Descriere
activeCount() Returnează numărul de Fir obiecte care sunt încă vii.
curent Thread() 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 Thread Class
start() Începe activitatea unui fir. Trebuie 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 suprascrisă de o clasă care extinde clasa Thread.
a te alatura() Acesta blochează execuția altui cod până când firul pe care a fost apelată metoda join() este terminat.

Povestea de fundal: Clasa Thread

Înainte de a începe să programați programe multithreaded folosind modulul threading, este esențial să înțelegeți 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 un multithreaded Python aplicației este de a declara o clasă care extinde clasa Thread și suprascrie metoda acesteia run().

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

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

  1. definește o clasă care extinde clasa Thread
  2. Ignorați __init__ constructor
  3. Ignorați alerga() metodă

Odată ce un obiect fir a fost realizat, start() Metoda poate fi utilizată pentru a începe executarea acestei activități și a te alatura() metoda poate fi folosită pentru a bloca orice alt cod până la terminarea activității curente.

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

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

Aceasta va fi rezultatul când executați codul de mai sus:

Povestea de fundal: Clasa Thread

EXPLICAȚIA CODULUI

Povestea de fundal: Clasa Thread

  1. Această parte este aceeași ca în exemplul nostru anterior. Aici, importați modulul time și thread, care sunt folosite pentru a gestiona execuția și întârzierile Python fire.
  2. În acest bit, creați o clasă numită threadtester, care moștenește sau extinde Fir clasa modulului de filetare. Aceasta este una dintre cele mai comune moduri de a crea fire în Python. Cu toate acestea, ar trebui să suprascrieți doar constructorul și alerga() metoda din aplicația dvs. După cum puteți vedea în exemplul de cod de mai sus, __init__ metoda (constructorul) a fost înlocuită. În mod similar, ați suprascris și alerga() metodă. Conține codul pe care doriți să-l executați într-un fir. În acest exemplu, ați apelat funcția thread_test().
  3. Aceasta este metoda thread_test(), care ia valoarea i ca argument, îl scade cu 1 la fiecare iterație și parcurge restul codului până când i devine 0. În fiecare iterație, afișează numele firului de execuție curent și intră în modul "wait" timp de secunde (care sunt, de asemenea, luate ca argument).
  4. thread1 = threadtester(1, „First Thread”, 1) Aici, creăm un fir și trecem cei trei parametri pe care i-am declarat în __init__. Primul parametru este id-ul firului de execuție, al doilea parametru este numele firului de execuție, iar al treilea parametru este contorul, care determină de câte ori ar trebui să ruleze bucla while.
  5. thread2.start() Metoda start este utilizată pentru a porni execuția unui fir de execuție. Intern, funcția start() apelează metoda run() a clasei dumneavoastră.
  6. thread3.join() Metoda join() blochează execuția altui cod și așteaptă până se termină firul pe care a fost chemat.

După cum știți deja, firele de execuție care se află în același proces au acces la memoria și datele acelui proces. Prin urmare, dacă mai multe fire de execuție încearcă să modifice sau să acceseze datele simultan, pot apărea erori.

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

Blocaje și condiții de cursă

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

  • Secțiunea critică: Este un fragment de cod care accesează sau modifică variabile partajate și trebuie executat ca o tranzacție atomică.
  • Comutare context: Este procesul pe care un procesor îl urmează pentru a stoca starea unui fir de execuție înainte de a trece de la o sarcină la alta, astfel încât aceasta să poată fi reluată din același punct ulterior.

Blocaje

Blocaje sunt cea mai de temut problemă cu care se confruntă dezvoltatorii atunci când scriu aplicații simultane/multithreaded PythonCea mai bună modalitate de a înțelege blocajele este prin utilizarea problemei clasice de informatică, cunoscută sub numele de Experiențe gastronomice Philosophers Problema.

Declarația problemei pentru filozofii de mese este următoarea:

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

Experiențe gastronomice Philosophers Problema

Experiențe gastronomice Philosophers Problema

În orice moment, un filozof trebuie fie să mănânce, fie să gândească.

Mai mult, un filozof trebuie să ia cele două furculițe adiacente lui (adică, furculița 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 drepte simultan.

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

În mod similar, într-un sistem concurent, apare un blocaj atunci când fire sau procese diferite (filozofi) încearcă să dobândească resursele de sistem partajate (furcuri) în același timp. Drept 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 concurență este o stare nedorită a unui program care apare atunci când un sistem efectuează două sau mai multe operații simultan. De exemplu, luați în considerare această buclă for simplă:

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

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

Acestea sunt cele două clase principale de probleme care pot apărea într-un sistem multithreaded sau distribuit Python aplicație. În secțiunea următoare, veți învăța cum să depășiți această problemă prin sincronizarea firelor de execuție.

Syncfire de cronizare

Pentru a face față condițiilor de cursă, blocajelor și altor probleme bazate pe fire, modulul de threading oferă Blocare obiect. Ideea este că atunci când un fir dorește acces la o anumită resursă, capătă o blocare pentru resursa respectivă. Odată ce un fir de execuție blochează o anumită resursă, niciun alt fir de execuție nu o poate accesa până când blocarea este eliberată. Ca urmare, modificările aduse resursei vor fi atomice, iar condițiile de cursă vor fi evitate.

O blocare este o primitivă de sincronizare de nivel scăzut implementată de fir de acțiune modul. În orice moment, o încuietoare poate fi într-una din două stări: blocat or deblocat. Acceptă două metode:

  1. dobândi(): Când starea de blocare este deblocată, apelarea metodei acquire() va schimba starea în blocată și va returna o valoare. Totuși, dacă starea este blocată, apelarea metodei acquire() este blocată până când metoda release() este apelată de un alt fir de execuție.
  2. eliberare(): Metoda release() este folosită pentru a seta starea la deblocat, adică pentru a elibera o blocare. Poate fi apelat prin orice thread, nu neapărat cel care a dobândit blocarea.

Iată un exemplu de utilizare a lacătelor în aplicațiile tale. Pornește-ți IDLE și tastați următoarele:

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

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

SyncFire de cronizare

EXPLICAȚIA CODULUI

SyncFire de cronizare

  1. Aici, pur și simplu creați o nouă blocare apelând la threading.Lock() funcția din fabrică. Pe plan intern, Lock() returnează o instanță a celei mai eficiente clase de Lock concrete care este menținută de platformă.
  2. În prima instrucțiune, obțineți blocarea apelând metoda acquire(). Când blocarea a fost acordată, imprimați „blocare dobândită” la consolă. Odată ce tot codul pe care doriți să îl ruleze firul de execuție s-a încheiat, eliberați blocarea apelând metoda release().

Teoria este bună, dar cum știi că blocarea a funcționat cu adevărat? Dacă te uiți la ieșire, vei vedea că fiecare dintre instrucțiunile print afișează exact câte o linie pe rând. Reamintiți-vă că, într-un exemplu anterior, ieșirile din print au fost aleatorii deoarece mai multe fire de execuție accesau metoda print() în același timp. Aici, funcția print este apelată numai după ce blocarea este achiziționată. Deci, ieșirile sunt afișate una câte una și linie cu linie.

În afară de încuietori, Python suportă și alte mecanisme pentru gestionarea sincronizării firelor de execuție, așa cum sunt enumerate mai jos:

  1. RLocks
  2. Semaphores
  3. Condiţii
  4. Evenimente și
  5. Bariere

Blocarea globală a interpretului (și cum să o faceți)

Înainte de a intra în detaliile PythonÎn cadrul GIL, să definim câțiva termeni care vor fi utili în înțelegerea secțiunii următoare:

  1. Cod legat de CPU: se referă la orice porțiune 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 intermediul sistemului de operare.
  3. CPython: este referința implementarea of Python și poate fi descris ca interpretul scris în C și Python (limbaj de programare).

În ce este GIL Python?

Blocare globală a interpretului (GIL) in Python este o blocare de proces sau un mutex utilizat în timpul gestionării proceselor. Se asigură că un fir de execuție poate accesa o anumită resursă la un moment dat și, de asemenea, previne utilizarea simultană a obiectelor și bytecode-urilor. Acest lucru aduce beneficii programelor cu un singur fir de execuție, cu o creștere a performanței. GIL în Python este foarte simplu și ușor de implementat.

O blocare poate fi folosită 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 o blocare globală pe fiecare proces de interpret, ceea ce înseamnă că fiecare proces tratează Python interpretul în sine ca resursă.

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

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

Datorită acestui fapt, un singur thread poate accesa interpretul în orice moment, ceea ce înseamnă că va exista doar un fir de execuție Python cod la un moment dat în timp.

Acest lucru este în regulă într-un procesor single-core, deoarece ar folosi time slicing (vezi prima secțiune a acestui tutorial) pentru a gestiona thread-urile. Cu toate acestea, în cazul procesoarelor multi-core, o funcție legată de CPU care se execută pe mai multe thread-uri 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?

CPython Colectorul de gunoi 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 crește 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 scade atunci când referința iese din domeniul de aplicare sau când este apelată instrucțiunea del. Când numărul de referințe al unui obiect ajunge la 0, acesta este colectat din gunoi, iar memoria alocată este eliberată.

Dar problema este că variabila număr de referințe este predispusă la condiții de rasă ca orice altă variabilă globală. Pentru a rezolva această problemă, dezvoltatorii de Python a decis să utilizeze blocarea globală a interpretorului. Cealaltă opțiune era adăugarea unei blocări la fiecare obiect, ceea ce ar fi dus la blocări intermediare și la creșterea suprasarcinii cauzate de apelurile acquire() și release().

Prin urmare, GIL este o restricție semnificativă pentru multithreaded Python programe care rulează operațiuni intense legate de CPU (făcându-le practic single-threaded). Dacă doriți să utilizați mai multe nuclee CPU în aplicația dvs., utilizați multiprocesare modul în schimb.

Întrebări frecvente

Folosește threading-ul pentru lucrări legate de I/O, cum ar fi apeluri de rețea, acces la fișiere sau interogări la baza de date, unde thread-urile așteaptă resurse externe. Folosește multiprocessing-ul pentru lucrări legate de CPU, cum ar fi calcule grele, deoarece rulează pe mai multe nuclee și ocolește GIL.

Nu în mod implicit. PEP 703 a introdus o versiune opțională cu thread liber în Python 3.13 care dezactivează GIL și Python Versiunea 3.14 continuă acest lucru. Versiunile standard includ în continuare GIL, așa că majoritatea programelor continuă să ruleze exact ca înainte.

Un fir de execuție daemon rulează în fundal și nu blochează ieșirea programului. Când rămân doar fire de execuție daemon, Python le oprește și se închide. Setați unul cu thread.daemon = True înainte de a apela start().

Clasa Thread nu returnează rezultatul run() direct. Stocați valoarea pe obiectul thread sau într-o coadă partajată sau utilizați concurrent.futures.ThreadPoolExecutor, a cărui metodă submit() returnează un Future pe care îl citiți cu result().

ThreadPoolExecutor este un instrument de asistență la nivel înalt care gestionează un pool de fire de execuție (thread-uri) worker. Trimiteți sarcini cu submit() sau map(), iar acesta gestionează crearea, reutilizarea și curățarea firelor de execuție, returnând obiecte Future care conțin fiecare rezultat.

Poți crea sute sau mii de fire de execuție, dar GIL permite executarea doar a unuia. Python bytecode în orice moment. Acestea se suprapun în continuare în timpul așteptărilor I/O, așa că multe fire de execuție simultane ajută programele legate de I/O mult mai mult decât pe cele legate de CPU.

Da, în principal pentru pașii legați de I/O, cum ar fi încărcarea datelor, apelarea API-urilor sau citirea fișierelor. Antrenamentul intens este legat de CPU, așa că biblioteci precum NumPy și PyTorch eliberează GIL în codul C sau se bazează pe multiprocesare pentru un paralelism real.

Da. GitHub Copilot completează automat modele comune precum subclasele Thread, utilizarea blocărilor și configurațiile ThreadPoolExecutor dintr-un comentariu sau un nume de funcție. Verificați întotdeauna codul generat pentru condițiile de concurență și gestionarea corectă a blocărilor înainte de a vă baza pe el.

Rezumați această postare cu: