Vai al contenuto principale

Multiprocessing in Python: guida a thread e processi

Impara a gestire thread e processi con il modulo multiprocessing di Python. Scopri le tecniche chiave per la programmazione parallela. Migliora l'efficienza del tuo codice con esempi.
· 7 min leggi

Scopri con l'IA

La libreria standard di Python include diversi pacchetti integrati che permettono agli sviluppatori di sfruttare subito i vantaggi del linguaggio.

Uno di questi pacchetti è il modulo multiprocessing, che consente al sistema di eseguire più processi simultaneamente. In altre parole, gli sviluppatori possono suddividere le applicazioni in thread più piccoli che possono essere eseguiti in modo indipendente dal codice Python. Questi thread o processi vengono poi assegnati al processore dal sistema operativo, permettendo l'esecuzione in parallelo e migliorando le prestazioni e l'efficienza dei tuoi programmi Python. 

Se termini come thread, processi, processori, ecc. non ti sono familiari, non preoccuparti. In questo articolo vedremo cos'è un processo, in cosa differisce da un thread e come usare il modulo multiprocessing. 

Cosa sono i processi in Python?

Capire il concetto di processi e thread è estremamente utile per comprendere meglio come un sistema operativo gestisce i programmi nelle varie fasi di esecuzione. Un processo è semplicemente un riferimento a un programma informatico. Ogni programma ha un processo associato. 

Mentre leggi questo articolo, è molto probabile che il tuo computer abbia molti processi in esecuzione nello stesso momento, anche se hai aperti solo pochi programmi – questo perché la maggior parte dei sistemi operativi esegue diversi compiti in background. Per esempio, potresti avere solo tre programmi aperti in questo momento, ma il tuo computer potrebbe avere più di 30 processi attivi in parallelo. 

Come controllare i processi attivi in esecuzione sul tuo computer dipende dal sistema operativo: 

  • Su Windows: Ctrl+Shift+Esc apre il Task Manager 
  • Su Mac: apri Spotlight e digita "Monitoraggio Attività", quindi premi Invio  
  • Su Linux: apri il menu Applicazioni e cerca Monitor di sistema. 

Python fornisce accesso a veri processi a livello di sistema. Istanziando un oggetto della classe Process dal modulo multiprocessing, gli sviluppatori possono riferirsi al processo nativo sottostante usando Python. Quando un processo viene avviato, dietro le quinte viene creato un nuovo processo nativo. Il ciclo di vita di un processo Python consiste in tre fasi: l'inizializzazione di un nuovo processo, il processo in esecuzione e il processo terminato – le vedremo tutte.

Tutti i processi sono composti da uno o più thread. Ricordi quando abbiamo detto che “un processo è semplicemente un riferimento a un programma”? Ecco, ogni programma Python è un processo che consiste in un thread predefinito chiamato thread principale (main thread). Il thread principale è responsabile dell'esecuzione delle istruzioni all'interno dei tuoi programmi Python. Tuttavia, è importante notare che processi e thread sono diversi. 

Multiprocessing vs Threading

Più nello specifico, un'istanza dell'interprete di Python – lo strumento che converte il codice scritto in Python nel linguaggio che un computer può comprendere – equivale a un processo. Un processo consiste di almeno un thread, chiamato “main thread” in Python, anche se all'interno dello stesso processo possono essere creati altri thread – tutti i thread creati in un processo apparterranno a quel processo. 

Il thread rappresenta il modo in cui il tuo programma Python verrà eseguito e, una volta terminati tutti i thread non in background, il processo Python terminerà. 

  • Process: un processo è un'istanza dell'interprete Python che consiste di almeno un thread chiamato main thread. 
  • Thread: una rappresentazione di come un programma Python viene eseguito all'interno di un processo Python.

Python offre due classi molto simili che ci danno più controllo su processi e thread: multiprocessing.Process e threading.Thread.

Vediamo alcune somiglianze e differenze. 

Somiglianze

Concorrenza

La concorrenza è un concetto in cui parti diverse di un programma possono essere eseguite fuori ordine o in ordine parziale senza che il risultato finale venga influenzato. Entrambe le classi sono state pensate inizialmente per la concorrenza. 

Supporto per primitive di concorrenza

Le classi multiprocessing.Process e threading.Thread supportano le stesse primitive di concorrenza – strumenti che consentono la sincronizzazione e il coordinamento di thread e processi. 

API uniforme

Una volta compresa l'API di multiprocessing.Process, puoi trasferire quella conoscenza all'API di threading.Thread, e viceversa. Sono state progettate appositamente così. 

Differenze

Funzionalità

Nonostante le API siano le stesse, processi e thread sono diversi. Un processo è un livello di astrazione più alto rispetto a un thread: un processo è un riferimento a un programma, e un thread appartiene a un processo. Questa differenza è intrinseca alle classi. Di conseguenza, le classi rappresentano due funzioni native differenti gestite dal sistema operativo sottostante. 

Accesso allo stato condiviso

Le due classi accedono allo stato condiviso in modo diverso. Poiché i thread appartengono a un processo, possono condividere la memoria all'interno di un processo. Quindi, una funzione eseguita in un nuovo thread ha comunque accesso agli stessi dati e allo stesso stato del processo. Il modo in cui i thread condividono lo stato tra loro è noto come “memoria condivisa” ed è piuttosto diretto. Al contrario, condividere lo stato tra processi è molto più complesso: lo stato deve essere serializzato e trasmesso tra processi. In altre parole, i processi non usano la memoria condivisa per condividere lo stato perché hanno memorie separate. Invece, i processi comunicano tramite una tecnica chiamata “comunicazione inter-processo” e, per farlo in Python, servono strumenti espliciti come multiprocessing.Pipe o multiprocessing.Queue. 

GIL

Il Global Interpreter Lock (GIL) di Python è un lock che consente a un solo thread di avere il controllo sull'interprete Python. I thread multipli sono soggetti al GIL, il che spesso rende una cattiva idea usare Python per il multithreading: la vera esecuzione multi-core tramite multithreading non è supportata da Python sull'interprete CPython. I processi, invece, non sono soggetti al GIL perché il GIL è utilizzato all'interno di ciascun processo Python, ma non tra processi diversi. 

Quanto ai casi d'uso, il multiprocessing spesso supera il threading negli scenari in cui il programma è intensivo sulla CPU e non deve eseguire IO o interazione con l'utente. Il threading è la soluzione migliore per programmi vincolati dall'IO o dalla rete e negli scenari in cui l'obiettivo è rendere l'applicazione più reattiva. 

I vantaggi del multiprocessing in Python

Pensa a un processore come a un'imprenditrice. Man mano che l'azienda cresce, aumentano i compiti da gestire per tenere il passo con la crescita. Se l'imprenditrice decidesse di occuparsi di tutti questi compiti da sola (cioè, contabilità, vendite, marketing, innovazione, ecc.), rischierebbe di compromettere l'efficienza e le prestazioni complessive dell'azienda, dato che una sola persona può fare solo tanto in un dato momento. Per esempio, prima di passare ai compiti di innovazione, dovrebbe interrompere quelli di vendita: questo è noto come eseguire i compiti “in sequenza”. 

La maggior parte degli imprenditori sa che cercare di fare tutto da soli è una cattiva idea. Di conseguenza, di solito compensano il crescente numero di compiti assumendo dipendenti per gestire vari reparti. In questo modo, i compiti possono essere svolti in parallelo, cioè un compito non deve essere interrotto perché ne venga eseguito un altro. Assumere più persone per svolgere compiti specifici è simile a usare più processori per eseguire operazioni. Per esempio, i progetti di computer vision sono piuttosto impegnativi perché in genere richiedono di elaborare molti dati di immagini, un'operazione che richiede tempo: per accelerarla, potresti elaborare più immagini in parallelo. 

Possiamo quindi dire che il multiprocessing è utile per rendere i programmi più efficienti dividendo e assegnando i compiti a processori diversi. Il modulo multiprocessing di Python semplifica ulteriormente la cosa fungendo da strumento di alto livello per aumentare l'efficienza dei tuoi programmi assegnando i compiti a processi diversi. 

Nozioni di base del modulo Multiprocessing di Python 

Abbiamo detto che il ciclo di vita di un processo Python consiste in tre fasi: il nuovo processo, il processo in esecuzione e il processo terminato. In questa sezione approfondiremo ciascuna fase del ciclo di vita e forniremo esempi di codice. Puoi accedere al codice per seguire passo passo andando al nostro quaderno DataLab. 

Il nuovo processo 

Un nuovo processo può essere definito come il processo creato istanziando un oggetto della classe Process. Quando assegniamo l'oggetto Process a una variabile, viene generato un processo figlio. 

from multiprocessing import Process

# Create a new process
process = Process()

Al momento, la nostra istanza di processo non fa nulla perché abbiamo inizializzato un oggetto Process vuoto. Possiamo modificare la configurazione del nostro oggetto Process per eseguire una funzione specifica passando la funzione che vogliamo eseguire in un processo diverso al parametro target della classe. 

# Create a new process with a specified function to execute.
def example_function():
    pass

new_process = Process(target=example_function)

Se anche la nostra funzione target avesse dei parametri, li passeremmo semplicemente al parametro args dell'oggetto Process come tupla.

Suggerimento: impara a scrivere funzioni in Python con il corso interattivo Writing Functions in Python. 

# Create a new process with specific function to execute with args.
def example(args):
    pass

process = Process(target=example, args=("Hi",))

Nota che abbiamo soltanto creato un nuovo processo, ma non è ancora in esecuzione. 

Vediamo come possiamo avviare un nuovo processo.  

Il processo in esecuzione 

Avviare un nuovo processo è piuttosto semplice: basta chiamare il metodo start() dell'istanza di processo. 

# Run the new process
process.start()

 Questa azione avvia l'attività del processo chiamando il metodo run() dell'istanza Process dietro le quinte. Il metodo run() è anche responsabile della chiamata della funzione personalizzata specificata nel parametro target dell'istanza Process (se specificata).

Ricorda che, come detto all'inizio del tutorial, ogni processo ha almeno un thread chiamato main thread – è l'impostazione predefinita. Quindi, quando un processo figlio viene avviato, viene creato e avviato il thread principale per quel processo figlio. Il thread principale è responsabile dell'esecuzione di tutto il nostro codice nel processo figlio. 

Possiamo verificare che la nostra istanza di processo sia attiva dal momento in cui il metodo start() restituisce fino a quando il processo figlio termina usando il metodo is_alive() sulla nostra istanza di Process. 

Nota: se stai testando questo in un notebook, la chiamata a is_alive() deve essere nella stessa cella della chiamata al metodo start() per intercettare il processo in esecuzione. 

# Run the new process
process.start()

# Check process is running
process.is_alive()

"""
True
"""

Se il processo non fosse in esecuzione, la chiamata a is_alive() restituirebbe False. 

Il processo terminato

Quando la funzione run() restituisce o esce, il processo viene terminato: non dobbiamo necessariamente fare nulla in modo esplicito. In altre parole, puoi aspettarti che un processo venga terminato dopo che le istruzioni impostate come funzione target sono complete. Di conseguenza, il metodo is_alive() restituirebbe false. 

# Check process is terminated - should return false.
process.is_alive()

"""
False
"""

Tuttavia, un processo può anche essere terminato se incontra un'eccezione non gestita o viene sollevato un errore. Per esempio, se c'è un errore nella funzione impostata come target, il processo terminerà. 

# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return name

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

# Check process is running
process.is_alive()

"""
True

Process Process-15:
Traceback (most recent call last):
  File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
"""

L'oggetto Process dispone anche dei metodi terminate() e kill() che consentono di terminare un processo forzatamente. 

# Create a new process with a specific function to execute with args.
def example(args):
    split_args = list(args.split())
    # "name" variable is not in the function namespace - should raise error
    return split_args

# New process
process = Process(target=example, args=("Hi",))

# Running the new process
process.start()

if process.is_alive():
    process.terminate() # You can also use process.kill()
    print("Process terminated forcefully")

"""
Process terminated forcefully
"""

È importante notare che le terminazioni forzate non sono il modo consigliato per terminare un processo: potrebbero non chiudere in sicurezza tutte le risorse aperte o non salvare lo stato del programma necessario. Una soluzione migliore è usare uno spegnimento controllato con un flag booleano sicuro per i processi o uno strumento simile. 

Tutorial sul multiprocessing in Python

Ora che hai capito le basi del multiprocessing, lavoriamo su un esempio per mostrare come fare programmazione concorrente in Python. 

La funzione che creeremo stamperà semplicemente un messaggio, andrà in sleep per 1 secondo, poi stamperà un altro messaggio – approfondisci le funzioni in questo tutorial sulle funzioni in Python. 

import time

def do_something():
    print("I'm going to sleep")
    time.sleep(1)
    print("I'm awake") 

Il primo passo è creare un nuovo processo: ne creeremo due. 

# Create new child process

process_1 = Process(target=do_something)
process_2 = Process(target=do_something)

Ecco come apparirà un programma concorrente in un ambiente notebook: 

%%time

# Starts both processes
process_1.start()
process_2.start()

"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""

Dato l'output del programma, è abbastanza evidente che c'è un problema da qualche parte nel nostro codice. Il timer viene stampato a metà del primo processo e la seconda stampa non viene eseguita. 

Questo accade perché ci sono tre processi in esecuzione: il processo principale e process_1 e process_2. Il processo che tiene traccia del tempo e lo stampa è il processo principale. Affinché il nostro processo principale aspetti prima di stampare il tempo, dobbiamo chiamare il metodo join() sui nostri due processi dopo averli avviati.

Nota: se vuoi approfondire, dai un'occhiata a questa discussione su Stackoverflow. 

Vediamo il nostro nuovo snippet di codice: 

%%time
​
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
​
# Starts both processes
new_process_1.start()
new_process_2.start()
​
new_process_1.join()
new_process_2.join()

"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""

Problema risolto. 

Il wall time di questa esecuzione è stato leggermente più lungo della prima. Tuttavia, questa esecuzione ha completato le chiamate a entrambe le nostre funzioni target prima di restituire le informazioni sul tempo. Possiamo applicare lo stesso ragionamento per far girare più processi in concorrenza. 

Considerazioni finali sul multiprocessing in Python

In questo tutorial hai imparato come rendere i programmi Python più efficienti eseguendoli in concorrenza. In particolare, hai imparato: 

  • Cosa sono i processi e come puoi visualizzarli sul tuo computer.
  • Le somiglianze e le differenze tra i moduli multiprocessing e threading di Python.
  • Le basi del modulo multiprocessing e come eseguire un programma Python in concorrenza usando multiprocessing. 

Vuoi saperne di più sulla programmazione in Python? Dai un'occhiata al nostro percorso di carriera Python Programmer. Non è richiesta alcuna esperienza di programmazione e, al termine del percorso, avrai acquisito le competenze necessarie per sviluppare software, gestire i dati ed eseguire analisi avanzate in Python. 

Kurtis Pykes

Autore

Kurtis Pykes

Argomenti

Correlato