Conținut curs
Gestionarea erorilor și excepțiilor
0/1
Python Intermediar
Despre lecție

Obiective:

  • Diferența dintre programarea concurentă și cea paralelă
  • Utilizarea bibliotecii threading pentru a crea și gestiona fire de execuție
  • Utilizarea bibliotecii multiprocessing pentru 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:

python
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.

python
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:

python
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:

python
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:

python
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:

python
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.