MODULO 06 · CLASSE QUARTA

Concorrenza e Parallelismo

threading e multiprocessing in Python, e perché il training di una rete neurale ne ha bisogno

🎯 Obiettivi del modulo

1. Concorrenza vs Parallelismo

In TPSIT hai già incontrato il concetto di processo e thread nel sistema operativo. Qui li usiamo direttamente in Python per velocizzare i nostri programmi di data science.

Il GIL (Global Interpreter Lock): in CPython un solo thread alla volta esegue bytecode Python. Questo significa che threading non accelera il calcolo puro (CPU-bound), ma è perfetto quando il programma aspetta qualcosa (I/O-bound: rete, file, database). Per sfruttare davvero più core della CPU serve multiprocessing, che crea processi separati, ognuno con il proprio interprete e la propria memoria.
Caratteristicathreadingmultiprocessing
Adatto aTask I/O-boundTask CPU-bound
MemoriaCondivisaSeparata per processo
Overhead di creazioneBassoPiù alto
Limitato dal GILSìNo

2. Il modulo threading

Esempio: scaricare più "risorse" simulate in parallelo (I/O-bound, simulato con time.sleep).

import threading
import time

def scarica_dato(nome, secondi):
    print(f"Inizio download di {nome}")
    time.sleep(secondi)  # simula un'attesa di rete
    print(f"Fine download di {nome}")

risorse = [("dataset_A", 2), ("dataset_B", 1), ("dataset_C", 3)]

inizio = time.time()
threads = []
for nome, secondi in risorse:
    t = threading.Thread(target=scarica_dato, args=(nome, secondi))
    threads.append(t)
    t.start()

for t in threads:
    t.join()  # aspetta che tutti i thread finiscano

print(f"Tempo totale: {time.time() - inizio:.2f}s")  # ~3s invece di 6s

I tre "download" partono quasi insieme: il tempo totale è vicino al più lento dei tre, non alla somma.

3. Il modulo multiprocessing

Ora un caso CPU-bound reale: calcolare qualcosa di pesante su più array, distribuendo il lavoro su più processi/core.

import multiprocessing
import time
import numpy as np

def somma_quadrati(array):
    return np.sum(array ** 2)

if __name__ == "__main__":
    dati = [np.random.rand(5_000_000) for _ in range(4)]

    # Versione sequenziale
    inizio = time.time()
    risultati_seq = [somma_quadrati(d) for d in dati]
    print(f"Sequenziale: {time.time() - inizio:.2f}s")

    # Versione parallela con Pool di processi
    inizio = time.time()
    with multiprocessing.Pool(processes=4) as pool:
        risultati_par = pool.map(somma_quadrati, dati)
    print(f"Parallela: {time.time() - inizio:.2f}s")
Nota: con multiprocessing su Windows/macOS il blocco if __name__ == "__main__": è obbligatorio, perché ogni processo re-importa il file principale.

4. Calcolo parallelo e training di una rete neurale

Il legame con l'IA è diretto: addestrare una rete neurale significa ripetere milioni di volte la stessa operazione — moltiplicazioni di matrici e aggiornamento dei pesi — su grandi quantità di dati. Questo lavoro è "embarrassingly parallel": ogni esempio del batch può essere elaborato in modo indipendente.

Dove entra il parallelismo nel training

Una GPU è di fatto migliaia di "piccoli processori" che fanno la stessa operazione in parallelo su dati diversi: è lo stesso concetto del nostro multiprocessing.Pool, ma con granularità enormemente maggiore. Capire threading e multiprocessing in Python è quindi il primo passo per capire perché il deep learning moderno è nato solo quando l'hardware parallelo (GPU) è diventato accessibile.

💪 Esercizi

  1. Facile: usa threading per "scaricare" 5 file simulati con tempi diversi e misura il tempo totale, confrontandolo con la somma sequenziale.
  2. Medio: usa multiprocessing.Pool per calcolare la media di 8 array NumPy di grandi dimensioni, confrontando tempo sequenziale e parallelo con time.time().
  3. Difficile: dividi un dataset (array NumPy) di un milione di righe in 4 blocchi, calcola in parallelo una statistica per blocco (es. deviazione standard) e combina i risultati in un unico valore finale — è lo schema base del data parallelism.

📌 Riepilogo

Il GIL limita threading al solo I/O-bound; multiprocessing supera il GIL usando processi separati ed è la scelta giusta per il calcolo puro. Il training delle reti neurali sfrutta lo stesso principio del parallelismo — dividere il lavoro tra unità indipendenti — a scala molto più grande, tramite GPU e training distribuito su più macchine.

← Modulo 05: Regressione Multipla Modulo 07: Reti e Socket →