Î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 Pythonargs
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
șitime
. - Funcția
some_func
include instrucțiuniprint()
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ă casome_func
.threading.Thread(target=…)
creează un obiect thread. - Notă: Specifică numele funcției și nu un apel de funcție; utilizează
some_func
și nusome_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ă pethread2
și numără până la 5 începând de la 0. - Funcția
count_down
rulează pethread1
ș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. Folosindthreading.Thread(target=<obiect_apelabil>, args=(<tuplu_de_argumente>))
se creează un fir care execută obiectul apelabil cu argumentele specificate înargs
. - 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 metodajoin()
.
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!🎉