Corso
Se hai appena iniziato con Python e vuoi approfondire, segui il corso Introduction to Data Science in Python di DataCamp.

Introduzione
Il threading ti permette di eseguire in concorrenza parti diverse del tuo processo (Fonte: RealPython). Queste parti differenti sono di solito indipendenti e hanno un’unità di esecuzione separata appartenente allo stesso processo. Un processo non è altro che un programma in esecuzione che contiene unità individuali eseguibili in parallelo. Ad esempio, un browser web può essere un processo, un’applicazione che esegue più videocamere contemporaneamente può essere un processo; anche un videogioco è un esempio di processo.
All’interno di un processo entra in gioco il concetto di threading multiplo, o più comunemente multi-threading, in cui più thread lavorano insieme per raggiungere un obiettivo comune. Il vantaggio più importante dell’uso dei thread è che ti permette di eseguire il programma in parallelo.
Capiremo il concetto di threading con un esempio. Immagina di avere un’applicazione che conta il numero di auto che entrano ed escono dal parcheggio di un centro commerciale. Il tuo apparato ha diverse videocamere che monitorano entrate e uscite collegate a un dispositivo centrale. Ogni videocamera avrà un algoritmo per monitorare il flusso di auto, che apparterrà allo stesso processo o programma. Tuttavia, ciascuna videocamera, insieme all’algoritmo su cui viene eseguita, potrebbe far parte di un thread separato. Non solo: anche i frame letti dalla videocamera e l’algoritmo che fa le previsioni sui frame potrebbero essere due thread distinti.
Un altro esempio potrebbe essere un videogioco in cui il processo deve eseguire attività in parallelo come grafica, interazione con l’utente e networking (nei giochi multiplayer), perché deve rimanere sempre reattivo. Per farlo, deve utilizzare il concetto di multi-threading, dove ogni thread è responsabile dell’esecuzione di un compito indipendente e autonomo.
Un thread ha un proprio flusso di esecuzione, il che significa che nel processo possono accadere più cose contemporaneamente.
È importante notare che ogni processo ha almeno un thread, chiamato main thread. Se il tuo programma non definisce esplicitamente alcun thread, avrà comunque il thread principale, cioè il processo stesso.
Se guardi lo schema sopra, c’è un processo che include il thread principale con le variabili globali e tre thread diversi t1, t2 e t3. Ogni thread ha le proprie variabili locali e il proprio flusso di esecuzione del codice. I thread in genere condividono le risorse del processo: ad esempio, le risorse disponibili per il main thread saranno condivise anche tra i tre thread t1, t2 e t3. Tutti i thread avranno accesso alle variabili globali, pur avendo le loro variabili locali.
Esistono tipi diversi di thread:
- Thread kernel
- Thread utente
- Combinazione di thread kernel e utente
Vediamo ora alcuni vantaggi dell’avere il threading nel tuo programma:
-
Il multi-threading consente al programma di velocizzare l’esecuzione, a patto di avere più CPU.
-
Ti permette anche di svolgere altre attività mentre vengono eseguite operazioni di I/O, grazie a più thread o anche al main thread insieme a un singolo thread. Ad esempio, la velocità con cui i frame della videocamera vengono letti e poi elaborati dall’algoritmo sarà gestita da thread diversi. Quindi, l’algoritmo non dovrà attendere che il frame venga acquisito, e la parte di lettura dei frame non dovrà aspettare che l’algoritmo finisca per poter leggere il frame successivo.
- I thread all’interno dello stesso processo possono condividere memoria e risorse del thread principale.
Sfide del threading
-
Ricorda che Python si basa sull’implementazione CPython, che limita l’esecuzione a un solo thread per volta; quindi, il threading potrebbe non accelerare tutti i compiti. Il motivo principale è il Global Interpreter Lock (GIL).
Se vuoi saperne di più sul GIL, dai un’occhiata a questo tutorial.
-
Se vuoi velocizzare un compito intensivo per la CPU, il threading potrebbe non essere la soluzione migliore. In tali casi, il multi-processing è spesso più utile.
-
Per eseguire un programma composto da vari thread, è necessario passare rapidamente da un thread all’altro; quindi, la schedulazione va curata nei programmi con molti thread.
-
La condivisione delle risorse può essere un problema, poiché tutti i thread condividono le stesse risorse e la stessa memoria delle variabili globali. Di conseguenza, operazioni svolte in un thread potrebbero causare un errore di memoria in un altro thread, oppure un altro thread potrebbe non ottenere la memoria necessaria a svolgere il proprio compito.
Threading in Python
-
In Python, il modulo di threading è integrato, si chiama
threadinge può essere importato direttamente. -
Poiché in Python quasi tutto è rappresentato come oggetto, anche il threading è un oggetto in Python. Un thread è in grado di:
- contenere dati,
- essere archiviato in strutture dati come dizionari, liste, set, ecc.,
- essere passato come parametro a una funzione.
-
Un thread può anche essere eseguito come un processo.
-
Un thread in Python può avere vari stati come:
- Attesa,
- Bloccato.
Vediamo ora come implementare il threading in Python.
Modulo thread in Python3
Nota che Python3 è retrocompatibile con il modulo thread, presente in Python 2.7. In Python3, può essere importato come modulo _thread. Vediamo un esempio per capire il modulo _thread.
import _thread #thread module imported
import time #time module
Definiamo una funzione chiamata thread_delay, che prende due parametri in input, cioè il nome del thread e il ritardo. All’interno di questa funzione:
- Definirai un contatore a zero,
- poi userai un ciclo while che verrà eseguito per tre volte,
- all’interno del while inserirai
time.sleep()per aggiungere un ritardo; questo ti aiuterà a capire l’esecuzione del thread. Il ritardo sarà in secondi, - quindi incrementerai il contatore di 1,
- per verificare che l’esecuzione del thread proceda correttamente, stamperai il nome del thread e l’istante in cui viene eseguito.
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
Ora, per aggiungere la funzionalità di thread alla funzione sopra o per eseguirla in un thread, userai il metodo start_new_thread, che si trova nel modulo _thread.
Vediamo la docstring del metodo start_new_thread.
?_thread.start_new_thread

Passiamo quindi la funzione thread_delay senza parentesi e i due argomenti, cioè il nome del thread e il ritardo (per visualizzare l’esecuzione del thread, dato che è molto veloce).
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 3))
10916
t1 --------> 1588077856.4221454
t1 --------> 1588077857.4418247
t2 --------> 1588077858.422453
t1 --------> 1588077858.4431
t2 --------> 1588077861.4371448
t2 --------> 1588077864.438915
Dal risultato sopra puoi vedere che il thread t1 inizia a eseguire per primo. Nel frattempo, il thread t2 attende poiché ha un ritardo di 3 secondi e, non appena il ritardo termina, t2 viene eseguito; inoltre, il ritardo per t1 è di soli 1 secondo.
Ora cambiamo il ritardo di t2 a 5 secondi, così t2 partirà dopo che t1 avrà terminato l’esecuzione, dato che t1 impiegherà solo 3 secondi per completarla.
_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 5))
14632
t1 --------> 1588078001.4087512
t1 --------> 1588078002.4272866
t1 --------> 1588078003.432218
t2 --------> 1588078005.4082909
t2 --------> 1588078010.4268317
t2 --------> 1588078015.4283602
Implementare il threading usando il modulo Threading
Useremo lo stesso esempio di sopra, ma questa volta userai il modulo threading invece del modulo _thread.
import threading
import time
def thread_delay(thread_name, delay):
count = 0
while count < 3:
time.sleep(delay)
count += 1
print(thread_name, '-------->', time.time())
All’interno del modulo threading c’è una classe Thread, simile nello spirito alla funzione start_new_thread del modulo _thread.
Vediamo la docstring della classe Thread, che accetta diversi parametri come group, target (ad esempio una funzione), args, ecc.
?threading.Thread

Nel costruttore della classe Thread passerai la funzione target thread_delay e gli argomenti di quella funzione.
t1 = threading.Thread(target=thread_delay, args=('t1', 1))
t2 = threading.Thread(target=thread_delay, args=('t2', 3))
Nel modulo threading, per avviare o eseguire il thread si usa il metodo start(), che è semplicemente responsabile dell’esecuzione del thread.
?t1.start

t1.start()
t2.start()
t1 --------> 1588081121.7790291
t1 --------> 1588081122.7962046
t2 --------> 1588081123.7789369
t1 --------> 1588081123.7980876
t2 --------> 1588081126.7961557
t2 --------> 1588081129.7995002
Userai anche il metodo join, che significa attendere finché l’esecuzione di tutti i thread non è completa. Quindi, qualunque codice tu abbia scritto dopo il metodo join verrà eseguito solo quando questi thread saranno terminati.
?t1.join

t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
t1 --------> 1588081798.7118263
t1 --------> 1588081799.7231226
t2 --------> 1588081800.7129223
t1 --------> 1588081800.730732
t2 --------> 1588081803.7298765
t2 --------> 1588081806.7309563
Thread execution is complete!
Ora calcoliamo il volume di cubo e quadrato usando il concetto di threading, che è piuttosto semplice.
def volume_cube(a):
print ("Volume of Cube:", a*a*a)
def volume_square(a):
print ("Volume of Square:", a*a)
t1 = threading.Thread(target=volume_cube, args=(2))
t2 = threading.Thread(target=volume_square, args=(3))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Thread execution is complete!
Exception in thread Thread-9:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_square() argument after * must be an iterable, not int
Exception in thread Thread-8:
Traceback (most recent call last):
File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
self.run()
File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
TypeError: volume_cube() argument after * must be an iterable, not int
Come? Appena hai eseguito il metodo start, è comparso un errore. Questo perché il parametro args si aspetta una tupla e, dato che volume_cube e volume_square si aspettano un solo parametro, devi mettere una virgola nel parametro args dopo aver specificato il primo argomento delle funzioni.
t1 = threading.Thread(target=volume_cube, args=(2,))
t2 = threading.Thread(target=volume_square, args=(3,))
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Volume of Cube: 8
Volume of Square: 9
Thread execution is complete!
Vediamo ora come usare il threading come sottoclasse.
Threading come sottoclasse
In questa sezione imparerai a creare una sottoclasse dalla classe Thread, disponibile nel modulo threading, e poi a creare istanze di thread t1 e t2 a partire da quella classe.
Qui userai di nuovo la stessa funzione thread_delay.
class DataCampThread(threading.Thread):
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
print('Starting Thread:', self.name)
thread_delay(self.name,self.delay)
print('Execution of Thread:', self.name, 'is complete!')
t1 = DataCampThread('t1', 1)
t2 = DataCampThread('t2', 3)
t1.start()
t2.start()
t1.join()
t2.join()
print("Thread execution is complete!")
Starting Thread: t1
Starting Thread: t2
t1 --------> 1588089083.4879806
t1 --------> 1588089084.489222
t1 --------> 1588089085.4909666
Execution of Thread: t1 is complete!
t2 --------> 1588089085.4979634
t2 --------> 1588089088.499365
t2 --------> 1588089091.5012596
Execution of Thread: t2 is complete!
Thread execution is complete!
Ora capiamo passo passo il codice sopra:
-
Hai definito una classe
DataCampThreade, nell’argomento della classe, hai passato la classethreading.Thread. Lo fai perché vuoi creare una sottoclasse dal modulo threading. In questo modo puoi usare tutti i metodi disponibili nella classethreading.Thread, in modo analogo al concetto di ereditarietà. -
Poi hai definito il metodo init della classe DataCampThread e hai passato la funzione insieme al ritardo. L’init è un costruttore: in modo simile ai parametri passati alla classe
threading.Thread, qui li passi alla classeDataCampThread. -
Quindi inizializzi il metodo init della classe principale threading.Thread e poi assegni name e delay.
-
Successivamente, definisci il metodo
run; ricorda che qui hai definito il metodostart, che sotto il cofano chiama il metodorun. Dato che hai ereditato la classe principale, puoi modificare il metodorun. Quindi, quando chiami il metodostart, qualunque modifica tu abbia fatto inrunverrà ora applicata.
Conclusione
Congratulazioni per aver completato il tutorial.
Questo tutorial è stata un’introduzione di base al threading in Python. Tuttavia, il threading è un argomento molto ampio e fondamentale, e alcuni concetti potrebbero non essere stati approfonditi. Sentiti libero di esplorare altri metodi del threading, come active count, current thread, ecc.
Se vuoi saperne di più, consulta anche la documentazione di Python su Thread-based Parallelism.
Se hai appena iniziato con Python e vuoi approfondire, segui il corso Introduction to Data Science in Python di DataCamp.

