Questo è un riassunto del mio tutorial PyCon 2020. Potete trovare il video originale e il codice sorgente qui: https://github.com/santiagobasulto/pycon-concurrency-tutorial-2020
Questa è una rapida guida/tutorial su come scrivere efficacemente programmi concorrenti usando Python. La concorrenza in Python può confondere. Ci sono più moduli (threading
, _thread
, multiprocessing
, subprocess
). C’è anche la tanto odiata GIL, ma solo per CPython (PyPy e Jython non hanno una GIL). Le collezioni non sono thread safe, tranne che per alcuni dettagli di implementazione con CPython.
L’obiettivo di questo tutorial è di fornire raccomandazioni concrete su come approcciare ogni caso d’uso del codice concorrente (o parallelo).
Si noterà che, a differenza di altri tutorial Python più tradizionali “solo multithreading”, inizio con un riassunto sia dell’architettura dei computer che dei concetti dei sistemi operativi. Comprendere questi concetti di base è fondamentale per costruire correttamente programmi concorrenti. Per favore saltate queste sezioni se avete già familiarità con questi argomenti.
Questo tutorial è diviso nelle seguenti sezioni:
- Concurrency vs Parallelismo
- Architettura del computer
- Il ruolo del sistema operativo
- Threads con Python
- Sincronizzazione dei thread
- Multiprocessing
- Libri di alto livello:
concurrent.futures
eparallel
Concurrency vs Parallelismo
Il parallelismo è quando diversi compiti sono in esecuzione allo stesso tempo. È l’obiettivo finale dei programmi concorrenti. La concorrenza è meno del parallelismo, significa che stiamo avviando diversi compiti e li stiamo destreggiando nello stesso periodo di tempo. Tuttavia, in qualsiasi momento particolare, ne stiamo facendo solo uno alla volta.
Applicato alla cucina, questi sarebbero esempi di concorrenza e parallelismo.
Concurrency ha solo una persona in cucina:
- Inizia a tagliare le cipolle
- Inizia a riscaldare la padella
- Finisce di tagliare le cipolle
In questo caso è chiaro che non possiamo fare più cose contemporaneamente. Possiamo iniziarle tutte, ma dobbiamo rimbalzare avanti e indietro per controllarle.
Il parallelismo ha più persone in cucina:
- La persona 1 sta tagliando le cipolle
- La persona 2 sta tagliando i peperoni rossi
- La persona 3 aspetta che la padella si riscaldi
In questo caso ci sono più compiti che vengono fatti contemporaneamente.
Architettura del computer
Tutti i computer moderni possono essere semplificati usando l’architettura von Neumann, che ha 3 componenti principali: Calcolo (CPU), Memoria (RAM), I/O (dischi rigidi, reti, uscita video).
Il tuo codice eseguirà sempre compiti relativi a ciascuno di questi componenti. Alcune istruzioni sono “computazionali” ( x + 1
), alcune sono di memoria (x = 1
), e alcune sono di I/O (fp = open('data.csv')
).
Per comprendere correttamente i dettagli della concorrenza in Python, è importante tenere a mente i diversi “tempi di accesso” di questi componenti. Accedere alla CPU è molto più veloce che accedere alla RAM, per non parlare dell’I/O. C’è uno studio che confronta la latenza del computer con i tempi relativi umani: se un ciclo di CPU rappresenta 1 secondo di tempo umano, una richiesta di rete da San Francisco a Hong Kong richiede 11 anni.
La radicale differenza di tempo con queste “istruzioni” sarà fondamentale per discutere i thread e la GIL di Python.
Il ruolo del sistema operativo
Se stessi costruendo un corso universitario sulla Concurrency, avrei un’intera lezione solo sulla storia dei sistemi operativi. È l’unico modo per capire come si sono evoluti i meccanismi di concorrenza.
I primi sistemi operativi orientati al consumatore (si pensi al MS-DOS) non supportavano il multitasking. Al massimo, solo un programma veniva eseguito in un certo momento. Furono seguiti da sistemi di “multitasking cooperativo” (Windows 3.0). Questo tipo di multitasking assomigliava più ad un “ciclo di eventi” (pensate all’asyncio di Python) che ad un moderno programma multithread. In un sistema operativo multitasking cooperativo, l’applicazione è incaricata di “rilasciare” la CPU quando non è più necessaria e lasciare che altre applicazioni vengano eseguite. Questo ovviamente non ha funzionato, dato che non ci si può fidare di tutte le applicazioni (o anche dei vostri sviluppatori che siano abbastanza acuti da fare la chiamata giusta su quando rilasciare la CPU). I sistemi operativi moderni (Windows 95, Linux, ecc.) hanno cambiato il loro design di multitasking in “Preemptive Multitasking”, in cui il sistema operativo è quello incaricato di decidere quali applicazioni ricevono la CPU, quanto spesso e per quanto tempo.
Il sistema operativo quindi, è quello che decide quali processi vengono eseguiti, e per quanto tempo. Questo è ciò che permette all’app di sentirsi ancora in “multitasking” anche se ha solo 1 CPU. Lo Scheduler del sistema operativo sta commutando i processi avanti e indietro molto velocemente.
Nel seguente diagramma, 3 processi (P1, P2, P3) sono in esecuzione contemporaneamente, condividendo la stessa CPU (la linea blu è il tempo della CPU). P1 viene eseguito per un bel po’ di tempo prima di essere rapidamente passato a P2, e poi a P3, e così via.