Obiective:
- Diferența dintre programarea concurentă și cea paralelă
- Utilizarea bibliotecii
threadingpentru a crea și gestiona fire de execuție - Utilizarea bibliotecii
multiprocessingpentru a crea și gestiona procese - Sincronizarea accesului la resurse folosind primitive de sincronizare
- Exemple practice și aplicații ale programării concurente și paralele
Introducere: Programare concurentă vs. paralelă
Programarea concurentă și cea paralelă sunt două concepte folosite pentru a îmbunătăți performanța unui program prin rularea simultană a mai multor bucăți de cod.
În programarea concurentă, mai multe sarcini rulează simultan dar nu neapărat în paralel. Ele pot fi planificate și executate pe un singur procesor într-un mod care crește eficiența. Programarea asincronă și co-rutinele sunt exemple de programare concurentă.
În programarea paralelă, sarcinile sunt efectuate de fapt în paralel pe mai mulți procesori sau nuclee ale procesorului. Acest lucru permite o îmbunătățire semnificativă a performanței, în special pentru sarcini care pot fi împărțite în bucăți independente și executate pe mai multe nuclee de procesor.
Crearea și gestionarea firelor de execuție cu biblioteca threading
Biblioteca threading din Python furnizează o modalitate de a crea și gestiona fire de execuție. Firele de execuție permit rularea concurentă a mai multor sarcini în cadrul aceluiași proces. Acestea sunt ușoare și au un consum redus de resurse în comparație cu procesele.
Pentru a crea un fir de execuție, putem defini o funcție care va fi executată de către fir și apoi să inițializăm și să pornim un obiect Thread:
import threading
def afiseaza_mesaj(mesaj):
print(f'Firul de execuție: {threading.current_thread().name} - {mesaj}')
def main():
fir1 = threading.Thread(target=afiseaza_mesaj, args=('Salut!',), name='Fir1')
fir2 = threading.Thread(target=afiseaza_mesaj, args=('Bună ziua!',), name='Fir2')
fir1.start()
fir2.start()
fir1.join()
fir2.join()
main()
În acest exemplu, funcția afiseaza_mesaj va fi executată de către două fire de execuție diferite. Funcția start pornește firele de execuție, iar funcția join așteaptă ca firele să se termine înainte de a continua execuția.
Crearea și gestionarea proceselor cu biblioteca multiprocessing
Biblioteca multiprocessing din Python furnizează o modalitate de a crea și gestiona procese. Procesele permit rularea paralelă a mai multor sarcini în cadrul unor procese separate. Spre deosebire de firele de execuție, procesele au propriul lor spațiu de adresă și nu sunt afectate de Global Interpreter Lock (GIL) din Python, ceea ce le face mai potrivite pentru sarcini care necesită calcul intensiv.
import multiprocessing
def afiseaza_mesaj(mesaj):
print(f'Procesul: {multiprocessing.current_process().name} - {mesaj}')
def main():
proces1 = multiprocessing.Process(target=afiseaza_mesaj, args=('Salut!',), name='Proces1')
proces2 = multiprocessing.Process(target=afiseaza_mesaj, args=('Bună ziua!',), name='Proces2')
proces1.start()
proces2.start()
proces1.join()
proces2.join()
main()
În acest exemplu, funcția afiseaza_mesaj va fi executată de către două procese diferite. Funcția start pornește procesele, iar funcția join așteaptă ca procesele să se termine înainte de a continua execuția.
Sincronizarea accesului la resurse folosind primitive de sincronizare
Când mai multe fire de execuție sau procese accesează resurse partajate, este posibil să apară probleme de sincronizare, cum ar fi condițiile de cursă. Pentru a evita aceste probleme, putem utiliza primitive de sincronizare, cum ar fi blocarea (locks) și semafoarele.
Locks
Un lock este o primitivă de sincronizare care permite unui singur fir de execuție sau proces să acceseze o resursă la un moment dat. În Python, putem utiliza clasa Lock din biblioteca threading pentru a crea și gestiona blocări:
import threading
lock = threading.Lock()
def functie_cu_lock():
with lock:
# cod care accesează resurse partajate
def main():
# crearea și pornirea firelor de execuție
Semaphores
Un semafor este o primitivă de sincronizare care permite unui număr limitat de fire de execuție sau procese să acceseze simultan o resursă. În Python, putem utiliza clasa Semaphore din biblioteca threading pentru a crea și gestiona semafoare:
import threading
semafor = threading.Semaphore(3) # permite accesul simultan al 3 fire de execuție
def functie_cu_semafor():
with semafor:
# cod care accesează resurse partajate
def main():
# crearea și pornirea firelor de execuție
Exemple practice și aplicații ale programării concurente și paralele
Exemplu 1: Descărcarea de fișiere în paralel
Un caz de utilizare tipic pentru programarea concurentă și paralelă este descărcarea simultană a mai multor fișiere de pe internet. Acest lucru poate fi realizat folosind biblioteca threading pentru a crea fire de execuție separate pentru fiecare descărcare:
import threading
import requests
url_list = [...] # o listă cu URL-uri de fișiere
def descarca_fisier(url, nume_fisier):
response = requests.get(url)
with open(nume_fisier, 'wb') as f:
f.write(response.content)
def main():
fire = []
for index, url in enumerate(url_list):
nume_fisier = f'fisier_{index}.txt'
fir = threading.Thread(target=descarca_fisier, args=(url, nume_fisier))
fire.append(fir)
fir.start()
for fir in fire:
fir.join()
main()
Exemplu 2: Aplicarea unei funcții pe o listă de elemente în paralel
Un alt caz de utilizare este aplicarea unei funcții pe o listă de elemente în paralel. Acest lucru poate fi realizat folosind biblioteca multiprocessing pentru a crea procese separate pentru fiecare element:
import multiprocessing
elemente = [...] # o listă cu elemente
rezultate = multiprocessing.Manager().list() # o listă partajată pentru a stoca rezultatele
def aplica_functie(element):
# aplică o funcție pe element și adaugă rezultatul în lista partajată
rezultat = ... # calculează rezultatul
rezultate.append(rezultat)
def main():
procese = []
for element in elemente:
proces = multiprocessing.Process(target=aplica_functie, args=(element,))
procese.append(proces)
proces.start()
for proces in procese:
proces.join()
print(list(rezultate))
main()
Aceste exemple ilustrează modul în care programarea concurentă și paralelă poate fi utilizată în Python pentru a îmbunătăți performanța în cazul sarcinilor care pot fi executate simultan sau în paralel.