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

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 Thread?

Un fir este o unitate de execuție a programării concurente. Multithreadingul este o tehnică care permite unui procesor 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.

În ce este Multithreadingul Python?

Multithreading în Python programarea este o tehnică binecunoscută în care mai multe fire dintr-un proces își împărtășesc spațiul de date cu firul principal, ceea ce face schimbul de informații și comunicarea în cadrul firelor de execuție ușoare și eficiente. Firele sunt mai ușoare decât procesele. Firele multiple se pot executa 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?

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 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 partajează datele procesului respectiv ș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 acceptă construcții atât pentru multiprocesare, cât și pentru multithreading. În acest tutorial, vă veți concentra în primul rând pe implementare multithread aplicații cu python. Există două module principale care pot fi folosite pentru a gestiona fire Python:

  1. fir modul, și
  2. filetat modul

Cu toate acestea, în python, există și ceva numit blocare globală a interpretului (GIL). Nu permite un câștig mare 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 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 tratat 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

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

Modulul Thread

EXPLICAȚIA CODULUI

  1. Aceste instrucțiuni importă modulul de timp și fir care sunt utilizate pentru a gestiona execuția și întârzierea fișierului 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 rulează o buclă while pentru patru iterații și tipărește numele firului care a numit-o. Odată ce iterația este completă, se tipărește un mesaj care spune că firul de execuție a încheiat execuția.
  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. Acest lucru va crea un fir nou 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 lucru (thread_test) cu orice altă funcție pe care doriți să o rulați ca thread.

Modulul Threading

Acest modul este implementarea la nivel înalt a threading-ului în 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ă în viață
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ă codificați programe cu mai multe fire folosind modulul de threading, este esențial să înțelegeți despre clasa Thread. Clasa thread este clasa primară care definește șablonul și operațiunile unui thread în python.

Cea mai obișnuită modalitate de a crea o aplicație python cu mai multe fire este de a declara o clasă care extinde clasa Thread și înlocuiește metoda run() a acesteia.

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. definiți 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 a te alatura() metoda poate fi folosită pentru a bloca orice alt cod până la terminarea activității curente.

Acum, să încercăm să folosim modulul de threading pentru a implementa exemplul anterior. Din nou, dă foc 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 cu exemplul nostru anterior. Aici, importați modulul de timp și fir care sunt utilizate 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 de execuție î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 lui 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 care se execută în prezent și așteaptă secunde (care este, de asemenea, luat 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 de pornire este folosită pentru a porni execuția unui fir. 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 care se află în același proces au acces la memoria și datele procesului respectiv. Ca rezultat, 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 accesează date și secțiuni critice fără a verifica tranzacțiile de acces existente.

Blocaje și condiții de cursă

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

  • Secțiunea critică Este un fragment de cod care accesează sau modifică variabilele partajate și trebuie efectuată ca o tranzacție atomică.
  • Comutare de context Este procesul pe care un CPU îl urmează pentru a stoca starea unui fir î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

Blocaje sunt cea mai de temut problemă cu care se confruntă dezvoltatorii atunci când scriu aplicații concurente/multithreaded în python. Cel mai bun mod de a înțelege blocajele este prin utilizarea exemplului clasic de problemă 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 pe 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 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 această buclă simplă:

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

Dacă creezi n numărul de fire care rulează acest cod simultan, nu puteți determina valoarea lui i (care este partajată de fire) 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 preluată și modificată de un fir de execuție, se poate schimba între ele când un alt thread îl accesează.

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

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 modul. În orice moment, o blocare poate fi în una dintre cele 2 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 reveni. Totuși, dacă starea este blocată, apelul la achiziție() este blocat până când metoda release() este apelată de un alt fir.
  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 blocărilor în aplicațiile dvs. Aprinde-ț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 î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 instrucțiunile de tipărire tipărește exact o linie la un moment dat. Amintiți-vă că, într-un exemplu anterior, ieșirile de la print au fost întâmplătoare, deoarece mai multe fire accesau metoda print() în același timp. Aici, funcția de imprimare este apelată numai după ce blocarea este obținută. Deci, ieșirile sunt afișate pe rând și rând cu linie.

În afară de blocări, python acceptă și alte mecanisme pentru a gestiona sincronizarea firelor, după cum este enumerat 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 GIL-ului python, să definim câțiva termeni care vor fi utili pentru înțelegerea secțiunii următoare:

  1. Cod legat de CPU: se referă la orice fragment 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 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) în python este un proces de blocare sau un mutex folosit în timpul proceselor. Se asigură că un fir de execuție poate accesa o anumită resursă la un moment dat și, de asemenea, previne utilizarea obiectelor și a codurilor de octet în același timp. Acest lucru avantajează programele cu un singur thread într-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ă interpretul Python în sine ca o resursă.

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

  1. Interpretul Python creează un nou proces și generează firele
  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 fir poate accesa interpretul în orice moment, ceea ce înseamnă că va exista un singur fir care execută codul Python la un moment dat.

Acest lucru este în regulă într-un procesor cu un singur nucleu, deoarece ar folosi tăierea în timp (consultați prima secțiune a acestui tutorial) pentru a gestiona firele. Cu toate acestea, în cazul procesoarelor cu mai multe nuclee, 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 de fapt nu va folosi toate nucleele disponibile în același timp.

De ce a fost nevoie de GIL?

CPython Garbage Collector 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.). De asemenea, 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 gunoiul, 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 python au decis să folosească blocarea interpretului global. Cealaltă opțiune a fost să adăugați o blocare la fiecare obiect, ceea ce ar fi dus la blocaje și o suprasarcină crescută de la apelurile de achiziție () și eliberare ().

Prin urmare, GIL este o restricție semnificativă pentru programele Python cu mai multe fire care rulează operațiuni grele legate de CPU (făcându-le efectiv cu un singur thread). Dacă doriți să utilizați mai multe nuclee CPU în aplicația dvs., utilizați multiprocesare modul în schimb.

Rezumat

  • Python suportă 2 module pentru multithreading:
    1. __fir modul: 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 fir folosind modulul de threading, trebuie să faceți următoarele:
    1. Creați o clasă care extinde Fir clasă.
    2. Suprascrieți constructorul său (__init__).
    3. Suprascrie-i alerga() metodă.
    4. Creați un obiect din această clasă.
  • Un thread poate fi executat apelând la start() metodă.
  • a te alatura() metoda poate fi folosită pentru a bloca alte fire de execuție până când acest fir de execuție (cel pe care a fost apelat join) termină execuția.
  • O condiție de cursă apare atunci când mai multe fire de execuție accesează sau modifică o resursă partajată în același timp.
  • Poate fi evitat prin Syncfire de cronizare.
  • Python acceptă 6 moduri de sincronizare a firelor:
    1. Broaste
    2. RLocks
    3. Semaphores
    4. Condiţii
    5. Evenimente și
    6. Bariere
  • Blocările permit doar unui anumit fir care a dobândit blocarea să intre în secțiunea critică.
  • Un blocare are 2 metode principale:
    1. dobândi(): Setează starea de blocare la blocat. Dacă este apelat la un obiect blocat, acesta se blochează până când resursa este liberă.
    2. eliberare(): Setează starea de blocare la descuiat si se intoarce. Dacă este apelat la un obiect deblocat, acesta returnează false.
  • Blocarea interpretului global este un mecanism prin care doar 1 CPython procesul interpretului se poate executa la un moment dat.
  • A fost folosit pentru a facilita funcționalitatea de numărare a referințelor a lui CPythongunoiul lui s.
  • A face Python aplicații cu operațiuni grele legate de CPU, ar trebui să utilizați modulul de multiprocesare.