CodeGym /Corso Java /Python SELF IT /Modulo threading

Modulo threading

Python SELF IT
Livello 25 , Lezione 1
Disponibile

2.1 Modulo threading

La multithreading in Python è un modo di eseguire diversi threads (threads) simultaneamente, il che permette di utilizzare più efficacemente le risorse del processore, specialmente per le operazioni di I/O o altre attività che possono essere eseguite in parallelo.

Concetti di base della multithreading in Python:

Thread — è la più piccola unità di esecuzione che può operare parallelamente ad altri threads in un singolo processo. Tutti i threads in un processo condividono la memoria comune, permettendo di scambiare dati tra threads.

Processo — è un'istanza di un programma in esecuzione nel sistema operativo, con il proprio spazio di indirizzamento e risorse. A differenza dei threads, i processi sono isolati l'uno dall'altro e scambiano dati attraverso la comunicazione inter-processo (IPC).

GIL — è un meccanismo nell'interprete Python che impedisce l'esecuzione simultanea di più threads Python. GIL garantisce la sicurezza dell'esecuzione del codice Python, ma limita le prestazioni dei programmi multithreading sui processori multi-core.

Importante! Tieni presente che a causa del Global Interpreter Lock (GIL) la multithreading in Python potrebbe non fornire un significativo aumento delle prestazioni per attività compute-intensive, poiché GIL impedisce l'esecuzione simultanea di più threads Python su processori multi-core.

Modulo threading

Il modulo threading in Python fornisce un'interfaccia di alto livello per lavorare con i threads. Permette di creare e gestire threads, sincronizzarli e organizzare l'interazione tra di essi. Diamo un'occhiata ai componenti chiave e alle funzioni di questo modulo in modo più dettagliato.

Componenti principali del modulo threading

Entità per lavorare con i threads:

  • Thread — la classe principale per la creazione e gestione dei threads.
  • Timer — un timer per eseguire una funzione dopo un intervallo di tempo specificato.
  • ThreadLocal — permette di creare dati locali per il thread.

Meccanismo di sincronizzazione dei threads:

  • Lock — un primitivo di sincronizzazione per prevenire l'accesso simultaneo alle risorse comuni.
  • Condition — una variabile condizionale per una sincronizzazione dei threads più complessa.
  • Event — un primitivo per notifiche tra threads.
  • Semaphore — un primitivo per limitare il numero di threads, che possono eseguire un certo segmento contemporaneamente.
  • Barrier — sincronizza un numero specificato di threads, bloccandoli fino a quando tutti non raggiungono la barriera.

Di seguito ti parlerò di 3 classi per lavorare con i threads, il meccanismo di sincronizzazione dei threads invece non ti servirà a breve.

2.2 Classe Thread

La classe Thread è la classe principale per la creazione e la gestione dei threads. Ha 4 metodi principali:

  • start(): Avvia l'esecuzione del thread.
  • join(): Il thread corrente viene sospeso e attende la terminazione del thread avviato.
  • is_alive(): Restituisce True se il thread è in esecuzione.
  • run(): Metodo che contiene il codice che verrà eseguito nel thread. Da sovrascrivere quando si eredita dalla classe Thread.

È tutto molto più semplice di quanto sembri — un esempio di utilizzo della classe Thread.

Avvio di un semplice thread


import threading

def worker():
    print("Il thread del lavoratore è in esecuzione")
            
# Creazione di un nuovo thread
t = threading.Thread(target=worker) #creato un nuovo oggetto thread
t.start() #Avviato il thread
t.join() # Attende il completamento del thread
print("Il thread principale è terminato")
        

Dopo la chiamata del metodo start, la funzione worker inizierà la sua esecuzione. O, più precisamente, il suo thread verrà aggiunto all'elenco dei thread attivi.

Uso degli argomenti


import threading

def worker(number, text):
    print(f"Lavoratore {number}: {text}")
            
# Creazione di un nuovo thread con argomenti
t = threading.Thread(target=worker, args=(1, "Ciao"))
t.start()
t.join()
        

Per passare parametri a un nuovo thread basta semplicemente indicarli sotto forma di una tupla e assegnarli al parametro args. Quando si chiama la funzione specificata in target, i parametri verranno passati automaticamente.

Sovrascrivere il metodo run


import threading

class MyThread(threading.Thread):
    def run(self):
        print("Il thread personalizzato è in esecuzione")
            
# Creazione e avvio del thread
t = MyThread()
t.start()
t.join()
        

Ci sono due modi per specificare la funzione con cui avviare l'esecuzione di un nuovo thread — si può passare attraverso il parametro target quando si crea un oggetto Thread, oppure ereditare dalla classe Thread e sovrascrivere la funzione run. Entrambi i modi sono considerati legali e sono utilizzati costantemente.

2.3 Classe Timer

La classe Timer nel modulo threading è progettata per avviare una funzione dopo un intervallo di tempo specificato. Questa classe è utile per l'esecuzione di attività ritardate in un ambiente multithreading.

Il timer viene creato e inizializzato con una funzione da chiamare e un tempo di ritardo in secondi.

  • Il metodo start() avvia il timer che conta l'intervallo di tempo specificato e poi chiama la funzione indicata.
  • Il metodo cancel() permette di fermare il timer, se non è ancora stato attivato. È utile per prevenire l'esecuzione della funzione, se il timer non è più necessario.

Esempi di utilizzo:

Avvio di una funzione con ritardo

In questo esempio la funzione hello verrà chiamata 5 secondi dopo l'avvio del timer.


import threading

def hello():
    print("Ciao, mondo!")
            
# Creazione di un timer che chiama la funzione hello dopo 5 secondi
t = threading.Timer(5.0, hello)
t.start()  # Avvio del timer
        

Fermare il timer prima dell'esecuzione

Qui il timer verrà fermato prima che la funzione hello possa essere eseguita, e quindi non verrà stampato nulla.


import threading

def hello():
    print("Ciao, mondo!")
            
# Creazione di un timer
t = threading.Timer(5.0, hello)
t.start()  # Avvio del timer
            
# Fermare il timer prima dell'esecuzione
t.cancel()
        

Timer con argomenti

In questo esempio il timer chiamerà la funzione greet dopo 3 secondi e le passerà l'argomento "Alice".


import threading

def greet(name):
    print(f"Hello, {name}!")
            
# Creazione di un timer con argomenti
t = threading.Timer(3.0, greet, args=["Alice"])
t.start()
        

La classe Timer è utile per pianificare l'esecuzione di attività dopo un certo periodo di tempo. Allo stesso tempo, i timers non garantiscono un tempo di esecuzione assolutamente preciso, poiché dipende dal carico del sistema e dal gestore dei threads.

2.4 Classe ThreadLocal

La classe ThreadLocal è progettata per creare threads, che hanno i propri dati locali. È utile in applicazioni multithreading, quando ogni thread deve avere la propria versione dei dati, per evitare conflitti e problemi di sincronizzazione.

Ogni thread che utilizza ThreadLocal avrà le proprie copie indipendenti dei dati. I dati memorizzati in un oggetto ThreadLocal sono unici per ogni thread e non sono condivisi con altri threads. È utile per memorizzare dati utilizzati solo nel contesto di un singolo thread, come l'utente attuale in un'applicazione web o la connessione corrente con il database.

Esempi di utilizzo:

Uso principale

In questo esempio ogni thread assegna il proprio nome alla variabile locale value e lo stampa. Il valore di value è unico per ogni thread.


import threading

# Creazione di un oggetto ThreadLocal
local_data = threading.local()
            
def process_data():
    # Assegnazione del valore alla variabile locale del thread
    local_data.value = threading.current_thread().name
    # Accesso alla variabile locale del thread
    print(f'Valore in {threading.current_thread().name}: {local_data.value}')

threads = []
for i in range(5):
    t = threading.Thread(target=process_data)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Memorizzazione dei dati utente in un'applicazione web

In questo esempio ogni thread gestisce una richiesta per il proprio utente. Il valore user_data.user è unico per ogni thread.


import threading
# Creazione di un oggetto ThreadLocal
user_data = threading.local()
def process_request(user):
    # Assegnazione del valore alla variabile locale del thread
    user_data.user = user
    handle_request()

def handle_request():
    # Accesso alla variabile locale del thread
    print(f'Gestione della richiesta per l'utente: {user_data.user}')

threads = []
users = ['Alice', 'Bob', 'Charlie']
for user in users:
    t = threading.Thread(target=process_request, args=(user,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Queste erano le 3 classi più utili della libreria threading. È probabile che tu le utilizzerai nel tuo lavoro, mentre le altre classi — difficilmente. Ora tutti stanno passando alle funzioni asincrone e alla libreria asyncio. Ed è di questo che parleremo nei prossimi tempi.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION