Il modulo `threading` in Python
Questo articolo spiega il modulo threading in Python.
YouTube Video
Il modulo threading in Python
Il modulo threading in Python è una libreria standard che supporta la programmazione multithread. Utilizzando i thread, si possono eseguire più processi contemporaneamente, migliorando le prestazioni dei programmi, soprattutto nei casi che coinvolgono operazioni di blocco come le attese di I/O. A causa del Global Interpreter Lock (GIL) di Python, l'efficacia del multithreading è limitata per le operazioni dipendenti dalla CPU, ma funziona in modo efficiente per le operazioni dipendenti dall'I/O.
Le seguenti sezioni spiegano le basi dell'uso del modulo threading e come controllare i thread.
Uso base dei thread
Creare ed eseguire i thread
Per creare un thread e realizzare un'elaborazione concorrente, utilizza la classe threading.Thread. Specifica la funzione di destinazione per creare un thread ed eseguire quel thread.
1import threading
2import time
3
4# Function to be executed in a thread
5def worker():
6 print("Worker thread started")
7 time.sleep(2)
8 print("Worker thread finished")
9
10# Create and start the thread
11thread = threading.Thread(target=worker)
12thread.start()
13
14# Processing in the main thread
15print("Main thread continues to run")
16
17# Wait for the thread to finish
18thread.join()
19print("Main thread finished")- In questo esempio, la funzione
workerviene eseguita in un thread separato, mentre il thread principale continua a operare. Richiamando il metodojoin(), il thread principale aspetta che il sottothread termini.
Assegnare nomi ai thread
Dare nomi significativi ai thread rende più facile il logging e il debug. Puoi specificarlo con l'argomento name.
1import threading
2import time
3
4# Function to be executed in a thread
5def worker():
6 print("Worker thread started")
7 time.sleep(2)
8 print("Worker thread finished")
9
10t = threading.Thread(
11 target=worker,
12 args=("named-worker", 0.3),
13 name="MyWorkerThread"
14)
15
16t.start()
17
18print("Active threads:", threading.active_count())
19for th in threading.enumerate():
20 print(" -", th.name)
21
22t.join()threading.enumerate()restituisce un elenco dei thread attuali, utile per il debug e il monitoraggio dello stato.
Ereditare la classe Thread
Se desideri personalizzare la classe che esegue il thread, puoi definire una nuova classe ereditando la classe threading.Thread.
1import threading
2import time
3
4# Inherit from the Thread class
5class WorkerThread(threading.Thread):
6 def __init__(self, name, delay, repeat=3):
7 super().__init__(name=name)
8 self.delay = delay
9 self.repeat = repeat
10 self.results = []
11
12 def run(self):
13 for i in range(self.repeat):
14 msg = f"{self.name} step {i+1}"
15 print(msg)
16 self.results.append(msg)
17 time.sleep(self.delay)
18
19# Create and start the threads
20t1 = WorkerThread("Worker-A", delay=0.4, repeat=3)
21t2 = WorkerThread("Worker-B", delay=0.2, repeat=5)
22
23t1.start()
24t2.start()
25
26t1.join()
27t2.join()
28
29print("Results A:", t1.results)
30print("Results B:", t2.results)- In questo esempio, il metodo
run()è sovrascritto per definire il comportamento del thread, permettendo a ciascun thread di mantenere i propri dati. Questo è utile quando i thread eseguono processi complessi o quando si desidera che ciascun thread abbia dati indipendenti.
Sincronizzazione tra i thread
Quando più thread accedono contemporaneamente a risorse condivise, possono verificarsi data races. Per evitarlo, il modulo threading fornisce diversi meccanismi di sincronizzazione.
Blocco (Lock)
L'oggetto Lock viene utilizzato per implementare il controllo esclusivo delle risorse tra i thread. Quando un thread blocca una risorsa, gli altri thread non possono accedere a quella risorsa.
1import threading
2
3lock = threading.Lock()
4shared_resource = 0
5
6def worker():
7 global shared_resource
8 with lock: # Acquire the lock
9 local_copy = shared_resource
10 local_copy += 1
11 shared_resource = local_copy
12
13threads = [threading.Thread(target=worker) for _ in range(5)]
14
15for t in threads:
16 t.start()
17
18for t in threads:
19 t.join()
20
21print(f"Final value of shared resource: {shared_resource}")- In questo esempio, cinque thread accedono a una risorsa condivisa, ma si utilizza il
Lockper impedire a più thread di modificare i dati contemporaneamente.
Blocco rientrante (RLock)
Se lo stesso thread deve acquisire un blocco più volte, usa un RLock (blocco rientrante). Questo è utile per le chiamate ricorsive o per le chiamate di libreria che potrebbero acquisire blocchi in chiamate diverse.
1import threading
2
3rlock = threading.RLock()
4shared = []
5
6def outer():
7 with rlock:
8 shared.append("outer")
9 inner()
10
11def inner():
12 with rlock:
13 shared.append("inner")
14
15t = threading.Thread(target=outer)
16t.start()
17t.join()
18print(shared)- Con
RLock, lo stesso thread può riacquisire un blocco che già possiede, il che aiuta a prevenire deadlock nell'acquisizione annidata dei blocchi.
Condizione (Condition)
Condition è usato affinché i thread attendano fino al soddisfacimento di una condizione specifica. Quando un thread soddisfa una condizione, puoi chiamare notify() per notificare un altro thread, oppure notify_all() per notificare tutti i thread in attesa.
Di seguito è riportato un esempio di produttore e consumatore che utilizzano una Condition.
1import threading
2
3condition = threading.Condition()
4shared_data = []
5
6def producer():
7 with condition:
8 shared_data.append(1)
9 print("Produced an item")
10 condition.notify() # Notify the consumer
11
12def consumer():
13 with condition:
14 condition.wait() # Wait until the condition is met
15
16 item = shared_data.pop(0)
17 print(f"Consumed an item: {item}")
18
19# Create the threads
20producer_thread = threading.Thread(target=producer)
21consumer_thread = threading.Thread(target=consumer)
22
23consumer_thread.start()
24producer_thread.start()
25
26producer_thread.join()
27consumer_thread.join()- Questo codice utilizza una
Conditionaffinché il produttore notifica quando vengono aggiunti dati, e il consumatore aspetta quella notifica prima di prelevare i dati, ottenendo così la sincronizzazione.
Daemonizzazione dei Thread
I thread daemon vengono terminati forzatamente quando il thread principale termina. Mentre i thread normali devono attendere per terminare, i thread daemon si terminano automaticamente.
1import threading
2import time
3
4def worker():
5 while True:
6 print("Working...")
7 time.sleep(1)
8
9# Create a daemon thread
10thread = threading.Thread(target=worker)
11thread.daemon = True # Set as a daemon thread
12
13thread.start()
14
15# Processing in the main thread
16time.sleep(3)
17print("Main thread finished")- In questo esempio, il thread
workerè daemonizzato, quindi viene terminato forzatamente quando il thread principale termina.
Gestione dei thread con ThreadPoolExecutor
Oltre al modulo threading, è possibile utilizzare ThreadPoolExecutor dal modulo concurrent.futures per gestire un pool di thread ed eseguire compiti in parallelo.
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4def worker(seconds):
5 print(f"Sleeping for {seconds} second(s)")
6 time.sleep(seconds)
7 return f"Finished sleeping for {seconds} second(s)"
8
9with ThreadPoolExecutor(max_workers=3) as executor:
10 futures = [executor.submit(worker, i) for i in range(1, 4)]
11 for future in futures:
12 print(future.result())ThreadPoolExecutorcrea un pool di thread e processa i compiti in modo efficiente. Specificare il numero di thread da eseguire contemporaneamente conmax_workers.
Comunicazione di Eventi tra Thread
Usando threading.Event, è possibile impostare flag tra i thread per notificare ad altri thread il verificarsi di un evento.
1import threading
2import time
3
4event = threading.Event()
5
6def worker():
7 print("Waiting for event to be set")
8 event.wait() # Wait until the event is set
9
10 print("Event received, continuing work")
11
12thread = threading.Thread(target=worker)
13thread.start()
14
15time.sleep(2)
16print("Setting the event")
17event.set() # Set the event and notify the thread- Questo codice dimostra un meccanismo in cui il thread worker attende il segnale
Evente riprende l'elaborazione quando il thread principale chiamaevent.set().
Gestione delle eccezioni e terminazione dei thread
Quando si verificano eccezioni nei thread, esse non vengono propagate direttamente al thread principale, quindi è necessario un pattern per catturare e condividere le eccezioni.
1import threading
2import queue
3
4def worker(err_q):
5 try:
6 raise ValueError("Something bad")
7 except Exception as e:
8 err_q.put(e)
9
10q = queue.Queue()
11t = threading.Thread(target=worker, args=(q,))
12t.start()
13t.join()
14if not q.empty():
15 exc = q.get()
16 print("Worker raised:", exc)- Inserendo le eccezioni in una
Queuee recuperandole nel thread principale, puoi rilevare in modo affidabile i fallimenti. Se utilizziconcurrent.futures.ThreadPoolExecutor, le eccezioni vengono rilanciate confuture.result(), rendendo la loro gestione più semplice.
Il GIL (Global Interpreter Lock) e i suoi effetti
A causa del meccanismo del GIL (Global Interpreter Lock) in CPython, più bytecode Python non vengono effettivamente eseguiti contemporaneamente all'interno dello stesso processo. Per i compiti che richiedono molta CPU, come calcoli pesanti, è consigliato usare multiprocessing. D'altro canto, per i compiti I/O-bound come la lettura di file o la comunicazione di rete, threading funziona efficacemente.
Riepilogo
Utilizzando il modulo threading di Python, è possibile implementare programmi multithreaded ed eseguire più processi contemporaneamente. Con meccanismi di sincronizzazione come Lock e Condition, è possibile accedere in modo sicuro a risorse condivise ed eseguire sincronizzazioni complesse. Inoltre, utilizzando thread daemon o ThreadPoolExecutor, la gestione dei thread e il processo parallelo efficiente diventano più semplici.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.