Threading Python: un’introduzione – winadmin.it

In questo tutorial imparerai come utilizzare il modulo di threading integrato di Python per esplorare le capacità di multithreading in Python.

A partire dalle basi di processi e thread, imparerai come funziona il multithreading in Python, mentre comprendi i concetti di concorrenza e parallelismo. Imparerai quindi come avviare ed eseguire uno o più thread in Python utilizzando il modulo di threading integrato.

Iniziamo.

Processi e thread: quali sono le differenze?

Che cos’è un processo?

Un processo è qualsiasi istanza di un programma che deve essere eseguito.

Può essere qualsiasi cosa: uno script Python o un browser Web come Chrome a un’applicazione di videoconferenza. Se avvii Task Manager sul tuo computer e vai su Prestazioni -> CPU, sarai in grado di vedere i processi e i thread attualmente in esecuzione sui core della tua CPU.

Comprensione di processi e thread

Internamente, un processo ha una memoria dedicata che memorizza il codice e i dati corrispondenti al processo.

Un processo è costituito da uno o più thread. Un thread è la più piccola sequenza di istruzioni che il sistema operativo può eseguire e rappresenta il flusso di esecuzione.

Ogni thread ha il proprio stack e registri ma non una memoria dedicata. Tutti i thread associati a un processo possono accedere ai dati. Pertanto, i dati e la memoria sono condivisi da tutti i thread di un processo.

In una CPU con N core, N processi possono essere eseguiti in parallelo alla stessa istanza di tempo. Tuttavia, due thread dello stesso processo non possono mai essere eseguiti in parallelo, ma possono essere eseguiti contemporaneamente. Affronteremo il concetto di concorrenza e parallelismo nella prossima sezione.

Sulla base di ciò che abbiamo imparato finora, riassumiamo le differenze tra un processo e un thread.

FeatureProcessThreadMemoryMemoria dedicataMemoria condivisaModalità di esecuzioneParallelo, simultaneoConcorrente; ma non parallelExecution gestito da Operating SystemCPython Interpreter

Multithreading in Python

In Python, il Global Interpreter Lock (GIL) garantisce che solo un thread possa acquisire il blocco ed essere eseguito in qualsiasi momento. Tutti i thread dovrebbero acquisire questo blocco per essere eseguiti. Ciò garantisce che un solo thread possa essere in esecuzione, in un dato momento, ed evita il multithreading simultaneo.

Ad esempio, si considerino due thread, t1 e t2, dello stesso processo. Poiché i thread condividono gli stessi dati quando t1 legge un valore k particolare, t2 può modificare lo stesso valore k. Questo può portare a deadlock e risultati indesiderati. Ma solo uno dei thread può acquisire il blocco ed essere eseguito in qualsiasi istanza. Pertanto, GIL garantisce anche la sicurezza del thread.

Quindi, come possiamo ottenere capacità di multithreading in Python? Per capirlo, discutiamo i concetti di concorrenza e parallelismo.

Concorrenza vs. Parallelismo: una panoramica

Considera una CPU con più di un core. Nell’illustrazione seguente, la CPU ha quattro core. Ciò significa che possiamo avere quattro diverse operazioni in esecuzione in parallelo in un dato istante.

Se sono presenti quattro processi, ciascuno dei processi può essere eseguito in modo indipendente e simultaneo su ciascuno dei quattro core. Supponiamo che ogni processo abbia due thread.

Per capire come funziona il threading, passiamo dall’architettura del processore multicore a quella single-core. Come accennato, solo un singolo thread può essere attivo in una particolare istanza di esecuzione; ma il core del processore può passare da un thread all’altro.

Ad esempio, i thread legati all’I/O spesso attendono operazioni di I/O: lettura dell’input dell’utente, letture del database e operazioni sui file. Durante questo tempo di attesa, può rilasciare il blocco in modo che l’altro thread possa essere eseguito. Il tempo di attesa può anche essere una semplice operazione come dormire per n secondi.

In sintesi: durante le operazioni di attesa, il thread rilascia il blocco, consentendo al core del processore di passare a un altro thread. Il thread precedente riprende l’esecuzione al termine del periodo di attesa. Questo processo, in cui il core del processore passa da un thread all’altro contemporaneamente, facilita il multithreading. ✅

Se desideri implementare il parallelismo a livello di processo nella tua applicazione, considera invece l’utilizzo del multiprocessing.

Modulo Python Threading: Primi Passi

Python viene fornito con un modulo di threading che puoi importare nello script Python.

import threading

Per creare un oggetto thread in Python, puoi usare il costruttore Thread: threading.Thread(…). Questa è la sintassi generica che è sufficiente per la maggior parte delle implementazioni di threading:

threading.Thread(target=...,args=...)

Qui,

  • target è l’argomento della parola chiave che denota una richiamabile Python
  • args è la tupla di argomenti che il target accetta.

Avrai bisogno di Python 3.x per eseguire gli esempi di codice in questo tutorial. Scarica il codice e segui.

Come definire ed eseguire thread in Python

Definiamo un thread che esegue una funzione di destinazione.

La funzione di destinazione è some_func.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())

Analizziamo cosa fa lo snippet di codice sopra:

  • Importa il threading e i moduli temporali.
  • La funzione some_func ha istruzioni print() descrittive e include un’operazione di sospensione per due secondi: time.sleep(n) fa sì che la funzione vada in pausa per n secondi.
  • Successivamente, definiamo un thread thread_1 con l’obiettivo come some_func. threading.Thread(target=…) crea un oggetto thread.
  • Nota: specificare il nome della funzione e non una chiamata di funzione; usa some_func e non some_func().
  • La creazione di un oggetto thread non avvia un thread; chiamando il metodo start() sull’oggetto thread lo fa.
  • Per ottenere il numero di thread attivi, utilizziamo la funzione active_count().

Lo script Python è in esecuzione sul thread principale e stiamo creando un altro thread (thread1) per eseguire la funzione some_func in modo che il conteggio dei thread attivi sia due, come si vede nell’output:

# Output
Running some_func...
2
Finished running some_func.

Se osserviamo più da vicino l’output, vediamo che all’avvio di thread1, viene eseguita la prima istruzione print. Ma durante l’operazione di sospensione, il processore passa al thread principale e stampa il numero di thread attivi, senza attendere il completamento dell’esecuzione del thread1.

In attesa che i thread finiscano l’esecuzione

Se vuoi che thread1 termini l’esecuzione, puoi chiamare il metodo join() su di esso dopo aver avviato il thread. In questo modo si attenderà che thread1 termini l’esecuzione senza passare al thread principale.

import threading
import time

def some_func():
    print("Running some_func...")
    time.sleep(2)
    print("Finished running some_func.")

thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())

Ora, thread1 ha terminato l’esecuzione prima di stampare il conteggio dei thread attivi. Quindi solo il thread principale è in esecuzione, il che significa che il conteggio dei thread attivi è uno. ✅

# Output
Running some_func...
Finished running some_func.
1

Come eseguire più thread in Python

Quindi, creiamo due thread per eseguire due diverse funzioni.

Qui, count_down è una funzione che accetta un numero come argomento e conta alla rovescia da quel numero fino a zero.

def count_down(n):
    for i in range(n,-1,-1):
        print(i)

Definiamo count_up, un’altra funzione Python che conta da zero fino a un determinato numero.

def count_up(n):
    for i in range(n+1):
        print(i)

📑 Quando si utilizza la funzione range() con la sintassi range(start, stop, step), l’arresto del punto finale è escluso per impostazione predefinita.

– Per eseguire il conto alla rovescia da un numero specifico a zero, è possibile utilizzare un valore di incremento negativo pari a -1 e impostare il valore di arresto su -1 in modo da includere zero.

– Allo stesso modo, per contare fino a n, devi impostare il valore di stop su n + 1. Poiché i valori predefiniti di start e step sono rispettivamente 0 e 1, puoi usare range(n + 1) per ottenere la sequenza 0 tramite n.

Successivamente, definiamo due thread, thread1 e thread2 per eseguire rispettivamente le funzioni count_down e count_up. Aggiungiamo istruzioni di stampa e operazioni di sospensione per entrambe le funzioni.

Quando si creano gli oggetti thread, si noti che gli argomenti della funzione target devono essere specificati come una tupla, nel parametro args. Poiché entrambe le funzioni (count_down e count_up) accettano un argomento. Dovrai inserire una virgola in modo esplicito dopo il valore. Ciò garantisce che l’argomento venga comunque passato come una tupla, poiché gli elementi successivi vengono dedotti come None.

import threading
import time

def count_down(n):
    for i in range(n,-1,-1):
        print("Running thread1....")
        print(i)
        time.sleep(1)


def count_up(n):
    for i in range(n+1):
        print("Running thread2...")
        print(i)
        time.sleep(1)

thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()

In uscita:

  • La funzione count_up viene eseguita su thread2 e conta fino a 5 a partire da 0.
  • La funzione count_down viene eseguita su thread1 con un conto alla rovescia da 10 a 0.
# Output
Running thread1....
10
Running thread2...
0
Running thread1....
9
Running thread2...
1
Running thread1....
8
Running thread2...
2
Running thread1....
7
Running thread2...
3
Running thread1....
6
Running thread2...
4
Running thread1....
5
Running thread2...
5
Running thread1....
4
Running thread1....
3
Running thread1....
2
Running thread1....
1
Running thread1....
0

Puoi vedere che thread1 e thread2 vengono eseguiti alternativamente, poiché entrambi implicano un’operazione di attesa (sleep). Una volta che la funzione count_up ha finito di contare fino a 5, thread2 non è più attivo. Quindi otteniamo l’output corrispondente al solo thread1.

Riassumendo

In questo tutorial, hai imparato come utilizzare il modulo di threading integrato di Python per implementare il multithreading. Ecco un riassunto dei punti chiave:

  • Il costruttore Thread può essere utilizzato per creare un oggetto thread. L’utilizzo di threading.Thread(target=,args=()) crea un thread che esegue il callable di destinazione con gli argomenti specificati in args.
  • Il programma Python viene eseguito su un thread principale, quindi gli oggetti thread che crei sono thread aggiuntivi. Puoi chiamare la funzione active_count() restituisce il numero di thread attivi in ​​qualsiasi istanza.
  • È possibile avviare un thread utilizzando il metodo start() sull’oggetto thread e attendere che termini l’esecuzione utilizzando il metodo join().

Puoi codificare esempi aggiuntivi modificando i tempi di attesa, provando un’operazione di I/O diversa e altro ancora. Assicurati di implementare il multithreading nei tuoi prossimi progetti Python. Buona codifica!🎉