Python Threading: o introducere – tipstrick.ro

În acest ghid, vei aprofunda utilizarea modulului de threading integrat în Python, explorând potențialul multithreadingului în cadrul acestui limbaj de programare.

Vom începe prin a defini bazele proceselor și firelor de execuție, apoi vom analiza funcționarea multithreadingului în Python, înțelegând totodată conceptele de concurență și paralelism. Ulterior, vei învăța cum să inițiezi și să rulezi unul sau mai multe fire de execuție în Python, folosind modulul de threading încorporat.

Să începem explorarea acestui subiect!

Procese versus fire de execuție: care sunt deosebirile?

Ce reprezintă un proces?

Un proces este o instanță a unui program aflat în execuție.

Poate fi orice – de la un script Python sau un browser web, cum ar fi Chrome, la o aplicație de videoconferință. Dacă deschizi Task Manager pe computer și accesezi secțiunea Performanță -> CPU, vei observa procesele și firele de execuție care rulează în mod activ pe nucleele procesorului.

Clarificarea proceselor și a firelor de execuție

Un proces dispune intern de o zonă de memorie alocată, în care sunt stocate codul și datele sale specifice.

Un proces este alcătuit dintr-unul sau mai multe fire de execuție. Un fir este cea mai mică secvență de instrucțiuni care poate fi executată de sistemul de operare, reprezentând fluxul de execuție.

Fiecare fir are propria sa stivă și set de registre, dar nu și memorie dedicată. Toate firele asociate unui proces pot accesa aceleași date. Prin urmare, memoria și datele sunt partajate între toate firele aceluiași proces.

Pe un procesor cu N nuclee, N procese pot rula în paralel. Totuși, două fire ale aceluiași proces nu vor rula în paralel, dar pot rula simultan. Vom discuta despre concurență versus paralelism în secțiunea următoare.

În baza cunoștințelor acumulate, să recapitulăm diferențele dintre un proces și un fir de execuție.

Caracteristică Proces Fir de execuție
Memorie Memorie dedicată Memorie partajată
Mod de execuție Paralel, concurent Concurent, dar nu paralel
Gestionarea execuției Sistemul de operare Interpretatorul CPython

Multithreading în Python

În Python, Global Interpreter Lock (GIL) asigură că doar un singur fir de execuție poate obține blocarea și poate rula într-un anumit moment. Pentru a putea rula, toate firele de execuție trebuie să obțină această blocare. Acest lucru garantează că doar un singur fir este în execuție la un moment dat, evitând astfel multithreadingul simultan.

Să luăm ca exemplu două fire, t1 și t2, ale aceluiași proces. Deoarece firele partajează aceleași date, în timp ce t1 citește o valoare k, t2 poate modifica aceeași valoare k. Aceasta poate conduce la blocaje și rezultate neașteptate. Dar doar unul dintre fire poate obține blocarea și poate rula la un anumit moment. Astfel, GIL asigură și siguranța firelor.

Așadar, cum obținem capacități de multithreading în Python? Pentru a înțelege acest lucru, vom discuta despre conceptele de concurență și paralelism.

Concurență versus paralelism: o imagine generală

Să considerăm un procesor cu mai mult de un nucleu. În ilustrația de mai jos, procesorul are patru nuclee. Aceasta înseamnă că putem avea patru operații diferite care rulează în paralel în același timp.

Dacă avem patru procese, fiecare proces poate rula independent și simultan pe câte un nucleu. Să presupunem că fiecare proces are două fire de execuție.

Pentru a înțelege funcționarea threadingului, să trecem de la arhitectura multi-core la una single-core. După cum am menționat, doar un singur fir de execuție poate fi activ într-un anumit moment, dar nucleul procesorului poate comuta între fire.

De exemplu, firele de execuție asociate operațiunilor I/O așteaptă adesea finalizarea acestor operațiuni: citirea de la intrarea utilizatorului, citirea bazelor de date și operațiunile cu fișiere. În timpul acestor perioade de așteptare, firul poate elibera blocarea, permițând altui fir să ruleze. Timpul de așteptare poate fi și o operațiune simplă, cum ar fi o pauză de n secunde.

Pe scurt: în timpul operațiunilor de așteptare, firul de execuție eliberează blocarea, permițând nucleului procesorului să treacă la un alt fir. Firul anterior își reia execuția după încheierea perioadei de așteptare. Acest proces, în care nucleul procesorului comută între fire, facilitează multithreadingul. ✅

Dacă dorești să implementezi paralelism la nivel de proces în aplicația ta, ia în considerare utilizarea multiprocessingului.

Modulul Python Threading: primii pași

Python include un modul de threading pe care îl poți importa în scriptul tău.

import threading

Pentru a crea un obiect thread în Python, poți folosi constructorul Thread: threading.Thread(…). Aceasta este sintaxa generală, suficientă pentru majoritatea implementărilor de threading:

threading.Thread(target=...,args=...)

Aici,

  • target este argumentul cuvântului cheie care specifică un obiect apelabil Python
  • args este tuplul de argumente primite de țintă.

Vei avea nevoie de Python 3.x pentru a rula exemplele de cod din acest ghid. Descarcă codul și urmărește instrucțiunile.

Cum să definești și să rulezi fire în Python

Vom defini un fir care execută o funcție țintă.

Funcția țintă este some_func.

import threading
import time

def some_func():
    print("Rulează some_func...")
    time.sleep(2)
    print("S-a terminat rularea some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())

Să analizăm ce face fragmentul de cod de mai sus:

  • Importă modulele threading și time.
  • Funcția some_func include instrucțiuni print() descriptive și o operațiune de pauză de două secunde: time.sleep(n) determină funcția să intre în pauză timp de n secunde.
  • Apoi, definim thread_1 cu ținta setată ca some_func. threading.Thread(target=…) creează un obiect thread.
  • Notă: Specifică numele funcției și nu un apel de funcție; utilizează some_func și nu some_func().
  • Crearea unui obiect thread nu pornește execuția firului; apelarea metodei start() pe obiectul thread pornește execuția.
  • Pentru a obține numărul de fire active, folosim funcția active_count().

Scriptul Python rulează pe firul principal și creăm un nou fir (thread1) pentru a rula funcția some_func, astfel numărul de fire active este doi, după cum se observă în rezultat:

   
# Ieșire
Rulează some_func...
2
S-a terminat rularea some_func.
   
  

Observând mai atent ieșirea, vedem că la pornirea thread1, prima instrucțiune print se execută. Dar în timpul operațiunii de pauză, procesorul trece la firul principal și afișează numărul de fire active – fără a aștepta ca thread1 să termine execuția.

Așteptarea finalizării execuției firelor

Dacă dorești ca thread1 să își termine execuția, poți apela metoda join() după pornirea firului. Astfel, se va aștepta ca thread1 să finalizeze execuția înainte de a continua cu firul principal.

import threading
import time

def some_func():
    print("Rulează some_func...")
    time.sleep(2)
    print("S-a terminat rularea some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())

Acum, thread1 și-a terminat execuția înainte de a afișa numărul de fire active. Prin urmare, doar firul principal rulează, ceea ce înseamnă că numărul de fire active este unu. ✅


# Ieșire
Rulează some_func...
S-a terminat rularea some_func.
1
  

Cum să rulezi mai multe fire în Python

În continuare, vom crea două fire pentru a rula două funcții diferite.

Aici, count_down este o funcție care primește un număr ca argument și numără invers de la acel număr până la zero.

def count_down(n):
    for i in range(n,-1,-1):
        print(i)
  

Definim count_up, o altă funcție Python care numără de la zero până la un anumit număr.

def count_up(n):
    for i in range(n+1):
        print(i)
  

📑 Când folosești funcția range() cu sintaxa (start, stop, step), punctul final este exclus implicit.

– Pentru a număra înapoi de la un anumit număr la zero, poți utiliza un pas negativ de -1 și poți seta valoarea de oprire la -1, astfel încât zero să fie inclus.

– Similar, pentru a număra până la n, trebuie să setezi valoarea de oprire la n + 1. Deoarece valorile implicite ale startului și pasului sunt 0 și, respectiv, 1, poți folosi range(n+1) pentru a obține secvența de la 0 la n.

În continuare, definim două fire, thread1 și thread2, pentru a rula funcțiile count_down și, respectiv, count_up. Adăugăm instrucțiuni de afișare și operațiuni de pauză pentru ambele funcții.

Când creezi obiectele thread, observă că argumentele funcției țintă trebuie specificate ca un tuplu – la parametrul args. Deoarece ambele funcții (count_down și count_up) primesc un singur argument, va trebui să inserezi o virgulă după valoare. Acest lucru asigură că argumentul este transmis ca un tuplu, deoarece elementele ulterioare sunt deduse ca fiind None.

import threading
import time

def count_down(n):
    for i in range(n,-1,-1):
        print("Rulează thread1....")
        print(i)
        time.sleep(1)


def count_up(n):
    for i in range(n+1):
        print("Rulează thread2...")
        print(i)
        time.sleep(1)

thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()

În ieșire:

  • Funcția count_up rulează pe thread2 și numără până la 5 începând de la 0.
  • Funcția count_down rulează pe thread1 și numără invers de la 10 la 0.

# Ieșire
Rulează thread1....
10
Rulează thread2...
0
Rulează thread1....
9
Rulează thread2...
1
Rulează thread1....
8
Rulează thread2...
2
Rulează thread1....
7
Rulează thread2...
3
Rulează thread1....
6
Rulează thread2...
4
Rulează thread1....
5
Rulează thread2...
5
Rulează thread1....
4
Rulează thread1....
3
Rulează thread1....
2
Rulează thread1....
1
Rulează thread1....
0

Observi că thread1 și thread2 rulează alternativ, deoarece ambele includ o operațiune de așteptare (sleep). După ce funcția count_up a terminat de numărat până la 5, thread2 nu mai este activ. Astfel, obținem rezultatul corespunzător numai de la thread1.

Concluzii

În acest tutorial, ai învățat cum să folosești modulul de threading încorporat în Python pentru a implementa multithreading. Iată un rezumat al principalelor concluzii:

  • Constructorul Thread poate fi utilizat pentru a crea un obiect thread. Folosind threading.Thread(target=<obiect_apelabil>, args=(<tuplu_de_argumente>)) se creează un fir care execută obiectul apelabil cu argumentele specificate în args.
  • Programul Python rulează pe un fir principal, astfel încât obiectele thread pe care le creezi sunt fire suplimentare. Poți apela funcția active_count(), care returnează numărul de fire active la un moment dat.
  • Poți porni un fir folosind metoda start() pe obiectul thread și poți aștepta finalizarea execuției utilizând metoda join().

Poți experimenta cu exemple suplimentare prin modificarea timpilor de așteptare, încercând o operațiune I/O diferită și multe altele. Asigură-te că implementezi multithreading în proiectele tale viitoare în Python. Spor la codare!🎉