Introduzione
Il multiprocessing permette di eseguire codice in processi separati, sfruttando più core della CPU e aggirando il Global Interpreter Lock (GIL) di Python. A differenza dei thread, però, i processi non condividono automaticamente le variabili: ogni processo possiede uno spazio di memoria indipendente.
Questa separazione è un vantaggio importante, perché riduce il rischio che un processo modifichi accidentalmente lo stato di un altro. Tuttavia, quando i processi devono collaborare, è necessario usare strumenti specifici per comunicare. In questo tutorial vedremo due tecniche pratiche:
- Pipe, utile per inviare messaggi tra due processi;
- Value e Array, utili per condividere piccoli dati numerici in memoria.
Realizzeremo un esempio in cui un processo calcola statistiche su una sequenza di numeri e invia il risultato al processo principale tramite una pipe. Inoltre, useremo una variabile condivisa per comunicare lo stato di avanzamento dell’elaborazione.
Codice completo
from multiprocessing import Process, Pipe, Value
import time
def analizza_dati(dati, connessione, progresso):
"""
Esegue un´elaborazione e invia il risultato al processo principale.
"""
totale = 0
for indice, numero in enumerate(dati, start=1):
# Simuliamo un´elaborazione costosa
time.sleep(0.1)
totale += numero
# Value richiede accesso tramite .value
progresso.value = indice / len(dati)
media = totale / len(dati)
risultato = {
"somma": totale,
"media": media,
"elementi": len(dati)
}
# Invio del dizionario attraverso la pipe
connessione.send(risultato)
# Chiudiamo la connessione quando non serve più
connessione.close()
if __name__ == "__main__":
numeri = list(range(1, 11))
# La pipe restituisce due estremità:
# una per il processo principale e una per il processo figlio
estremita_principale, estremita_figlio = Pipe()
# Value("d", 0.0) crea un valore condiviso di tipo double
progresso = Value("d", 0.0)
processo = Process(
target=analizza_dati,
args=(numeri, estremita_figlio, progresso)
)
processo.start()
# Il processo principale controlla l´avanzamento
while processo.is_alive():
print(f"Progresso: {progresso.value:.0%}")
time.sleep(0.2)
# Ricezione del risultato inviato dal processo figlio
risultato = estremita_principale.recv()
processo.join()
print("nElaborazione completata")
print(f"Somma: {risultato[´somma´]}")
print(f"Media: {risultato[´media´]}")
print(f"Elementi analizzati: {risultato[´elementi´]}") Spiegazione
Creazione di una pipe
Pipe() crea un canale di comunicazione tra processi e restituisce due oggetti connessione. Nel nostro esempio, estremita_principale viene usata dal processo principale per ricevere dati, mentre estremita_figlio viene passata al processo secondario.
Il processo figlio invia un oggetto usando:
connessione.send(risultato) Il processo principale riceve lo stesso oggetto con:
risultato = estremita_principale.recv() Gli oggetti inviati attraverso una pipe vengono serializzati internamente. È quindi possibile spedire dizionari, liste, stringhe e molti altri oggetti Python compatibili con il meccanismo di serializzazione.
Attenzione a recv()
La chiamata recv() è bloccante: il processo rimane in attesa finché non arriva un messaggio. Questa caratteristica è utile quando il risultato è obbligatorio, ma può causare un blocco se il processo mittente termina senza inviare nulla.
Per scenari più complessi si possono usare anche poll() e un timeout:
if estremita_principale.poll(2):
risultato = estremita_principale.recv()
else:
print("Nessun risultato ricevuto entro due secondi") Condivisione di un valore numerico
Value crea una singola variabile presente in memoria condivisa. Il primo argomento indica il tipo di dato. Per esempio:
"i"indica un intero;"d"indica un numero in virgola mobile;"b"indica un valore booleano rappresentato come byte.
Nel codice usiamo progresso.value per aggiornare e leggere la percentuale di avanzamento. Senza .value, non accederemmo al contenuto della variabile condivisa.
Per condividere più valori numerici è possibile usare Array:
from multiprocessing import Array
valori_condivisi = Array("i", [0, 0, 0, 0])
valori_condivisi[0] = 42 Queste strutture sono efficienti, ma non sostituiscono una comunicazione complessa. Per scambiare messaggi strutturati, una pipe o una coda sono generalmente più adatte.
Perché usare if __name__ == "__main__"
Il blocco principale è indispensabile soprattutto su Windows e macOS, dove il multiprocessing può avviare nuovi processi importando nuovamente il modulo. Senza questo controllo, il codice che crea processi potrebbe essere eseguito ricorsivamente.
Best practice
- Chiudi le connessioni inutilizzate: chiamare
close()evita di lasciare risorse aperte. - Attendi sempre i processi: usa
join()per assicurarti che il processo figlio sia terminato. - Invia messaggi semplici: trasferire oggetti molto grandi attraverso una pipe può essere lento a causa della serializzazione.
- Proteggi i dati condivisi: se più processi modificano lo stesso
ValueoArray, usa unLockper evitare condizioni di gara. - Gestisci gli errori: il processo principale dovrebbe prevedere il caso in cui il processo figlio termini prima di inviare il risultato.
- Preferisci la memoria condivisa per dati piccoli: contatori, percentuali e flag sono buoni candidati; grandi dataset richiedono strategie più specializzate.
- Evita di condividere troppo stato: molti dati condivisi rendono il programma più difficile da testare e possono ridurre i vantaggi del multiprocessing.
Riepilogo
Nel multiprocessing ogni processo ha una memoria separata, ma Python offre strumenti per farli collaborare. Pipe è adatta alla comunicazione diretta tra due processi e consente di trasferire messaggi o risultati. Value e Array permettono invece di condividere piccoli dati numerici, come un contatore o lo stato di avanzamento.
La scelta dello strumento dipende dal tipo di informazione da trasferire: usa una pipe per messaggi e risultati, la memoria condivisa per dati semplici aggiornati frequentemente. In entrambi i casi, è fondamentale gestire correttamente chiusura delle risorse, sincronizzazione e terminazione dei processi.
