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

Obiective:

  • Introducere în biblioteca threading
  • Crearea și gestionarea firelor de execuție
  • Comunicarea între firele de execuție
  • Sincronizarea firelor de execuție
  • Gestionarea resurselor partajate
  • Exemple practice și aplicații ale bibliotecii threading

Introducere în biblioteca threading

Biblioteca threading din Python furnizează o modalitate de a crea și gestiona fire de execuție. Aceasta permite rularea paralelă a mai multor sarcini în cadrul unui singur proces. Firele de execuție au avantajul de a partaja spațiul de adresă al procesului și au un consum redus de resurse în comparație cu procesele separate. Cu toate acestea, din cauza Global Interpreter Lock (GIL) din Python, firele de execuție nu sunt întotdeauna eficiente în cazul sarcinilor care necesită calcul intensiv.

Crearea și gestionarea firelor de execuție

Pentru a crea un fir de execuție, putem defini o funcție care va fi executată de către firul de execuție ș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():
    thread1 = threading.Thread(target=afiseaza_mesaj, args=('Salut!',), name='Thread1')
    thread2 = threading.Thread(target=afiseaza_mesaj, args=('Bună ziua!',), name='Thread2')
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.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 de execuție să se termine înainte de a continua execuția.

Comunicarea între firele de execuție

Comunicarea între firele de execuție este importantă atunci când acestea trebuie să partajeze date sau să sincronizeze execuția. Biblioteca threading oferă diferite metode de comunicare între firele de execuție, cum ar fi:

  • Variabilele partajate: variabilele pot fi partajate între firele de execuție, deoarece acestea se execută în cadrul aceluiași proces și au același spațiu de adresă.
  • Queue: o coadă care permite firelor de execuție să comunice prin trimiterea și primirea de mesaje.

Variabile partajate

Următorul exemplu ilustrează utilizarea unei variabile partajate pentru a comunica între firele de execuție:

python
import threading

def modifica_variabila(variabila, valoare):
    variabila.append(valoare)
    print(f'Variabila modificată: {variabila}')

def main():
    variabila = []
    
    thread1 = threading.Thread(target=modifica_variabila, args=(variabila, 1), name='Thread1')
    thread2 = threading.Thread(target=modifica_variabila, args=(variabila, 2), name='Thread2')
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()

main()

Queue

Următorul exemplu ilustrează utilizarea unei cozi pentru a comunica între firele de execuție:

python
import threading
import queue

def produce_elemente(coada):
    for i in range(5):
        coada.put(i)
        print(f'Element adăugat: {i}')

def consuma_elemente(coada):
    while not coada.empty():
        element = coada.get()
        print(f'Element extras: {element}')

def main():
    coada = queue.Queue()
    
    thread_producator =threading.Thread(target=produce_elemente, args=(coada,), name='Producător')
    thread_consumator = threading.Thread(target=consuma_elemente, args=(coada,), name='Consumator')
    
    thread_producator.start()
    thread_consumator.start()
    
    thread_producator.join()
    thread_consumator.join()

main()

Sincronizarea firelor de execuție

Sincronizarea firelor de execuție este esențială pentru a evita condițiile de concurență și pentru a asigura o execuție corectă a programului. Biblioteca threading oferă diferite mecanisme de sincronizare, cum ar fi:

  • Locks: o metodă de a asigura accesul exclusiv la o resursă partajată.
  • Semaphores: o metodă de control al accesului simultan la o resursă partajată.
  • Conditions: o metodă de a sincroniza comportamentul a două sau mai multe fire de execuție.

Locks

Următorul exemplu ilustrează utilizarea unui lock pentru a asigura accesul exclusiv la o resursă partajată:

python
import threading

def adauga_elemente(lista, lock):
    for i in range(5):
        with lock:
            lista.append(i)
            print(f'Element adăugat: {i}')

def main():
    lista = []
    lock = threading.Lock()
    
    thread1 = threading.Thread(target=adauga_elemente, args=(lista, lock), name='Thread1')
    thread2 = threading.Thread(target=adauga_elemente, args=(lista, lock), name='Thread2')
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()

main()

Semaphores

Următorul exemplu ilustrează utilizarea unui semafor pentru a controla accesul simultan la o resursă partajată:

python
import threading
import time

def acceseaza_resursa(semafor):
    with semafor:
        print(f'Resursa accesată de: {threading.current_thread().name}')
        time.sleep(1)

def main():
    semafor = threading.Semaphore(2)
    
    threads = [threading.Thread(target=acceseaza_resursa, args=(semafor,), name=f'Thread{i}') for i in range(5)]

    for thread in threads:
        thread.start()
    
    for thread in threads:
        thread.join()

main()

Conditions

Următorul exemplu ilustrează utilizarea unei condiții pentru a sincroniza comportamentul a două fire de execuție:

python
import threading

def produc_elemente(lista, conditie):
    for i in range(5):
        with conditie:
            lista.append(i)
            print(f'Element adăugat: {i}')
            conditie.notify()

def consuma_elemente(lista, conditie):
    for _ in range(5):
        with conditie:
            conditie.wait()
            element = lista.pop(0)
            print(f'Element extras: {element}')

def main():
    lista = []
    conditie = threading.Condition()
    
    thread_producator = threading.Thread(target=produc_elemente, args=(lista, conditie), name='Producător')
    thread_consumator = threading.Thread(target=consuma_elemente, args=(lista, conditie), name='Consumator')
    
    thread_producator.start()
    thread_consumator.start()
    
    thread_producator.join()
    thread_consumator.join()

main()

Exemple practice și aplicații ale bibliotecii threading

Biblioteca threading poate fi utilizată pentru a implementa diverse aplicații și funcționalități, cum ar fi:

  • Servere web cu fire de execuție multiple pentru a gestiona cererile simultan.
  • Implementarea de algoritmi paraleli pentru a îmbunătăți performanța.
  • Realizarea de aplicații cu interfață grafică care necesită actualizări în timp real.

Un exemplu de aplicație care utilizează biblioteca threading ar putea fi un server web simplu care acceptă conexiuni simultane:

python
import socket
import threading

def gestioneaza_conexiune(client_socket, client_address):
    print(f'Conexiune acceptată de la: {client_address}')
    
    request_data = client_socket.recv(1024)
    print(f'Date primite: {request_data}')
    
    response = b'HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn<b>Salut!</b>'
    client_socket.sendall(response)
    
    client_socket.close()

def main():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 8080))
    server_socket.listen(5)
    
    print('Serverul a început să asculte pe portul 8080...')
    
    try:
        while True:
            client_socket, client_address = server_socket.accept()
            
            thread = threading.Thread(
                target=gestioneaza_conexiune,
                args=(client_socket, client_address),
                name=f'Thread-{client_address}'
            )
            thread.start()
    except KeyboardInterrupt:
        print('nServerul s-a oprit.')
    finally:
        server_socket.close()

main()

În acest exemplu, funcția gestioneaza_conexiune este responsabilă de gestionarea conexiunilor cu clienții. Funcția acceptă un socket client și adresa clientului ca argumente. Serverul primește date de la client și trimite înapoi un răspuns HTTP simplu.

Funcția principală main creează un socket server, îl leagă de adresa ‘localhost’ și portul 8080, apoi începe să asculte pentru conexiuni. Pentru fiecare conexiune acceptată, serverul creează un nou fir de execuție care va gestiona conexiunea cu clientul.

Acest exemplu ilustrează cum biblioteca threading poate fi folosită pentru a crea un server web care acceptă și gestionează simultan mai multe conexiuni.