11.1 Global Interpreter Lock
Global Interpreter Lock (GIL)
è un meccanismo
nell'interprete CPython che garantisce che solo un thread esegua byte-code
Python alla volta. Il GIL
impedisce l'esecuzione parallela
dei thread, il che può influire negativamente sulle prestazioni
dei programmi multithreading, specialmente sui processori multi-core.
Motivi per cui esiste il GIL
Semplificazione della gestione della memoria: il GIL
semplifica la gestione della memoria e il garbage collection, rendendo
Python più semplice da implementare.
Sicurezza dei thread: il GIL
previene le condizioni
di race, rendendo l'esecuzione del codice sicura per i thread senza la
necessità di utilizzare i lock.
Problemi causati dal GIL
Prestazioni limitate: i programmi multithreading che eseguono
compiti intensivi dal punto di vista computazionale non possono
sfruttare appieno i vantaggi dei processori multi-core a causa delle
limitazioni del GIL
.
Distorsione delle prestazioni: i programmi che utilizzano intensamente i thread per eseguire compiti possono incontrare un peggioramento delle prestazioni a causa del contesto switch tra i thread.
Attualmente ci sono 4 modi principali per "aggirare il GIL":
11.2 Utilizzo del multiprocessing (multiprocessing)
Il modulo multiprocessing
permette di creare
processi che vengono eseguiti in parallelo e non sono limitati dal
GIL
, poiché ogni processo ha il proprio interprete Python
e memoria.
Esempio:
import multiprocessing
def worker(num):
print(f'Worker: {num}')
def main():
processes = []
for i in range(5):
p = multiprocessing.Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
main()
11.3 Programmazione asincrona
La programmazione asincrona utilizzando asyncio
permette
di eseguire compiti in parallelo senza bloccare il thread principale.
Anche se non aggira il GIL
nel vero senso della parola,
permette di utilizzare in modo efficiente il tempo di attesa per eseguire
altri compiti.
Esempio:
import asyncio
async def main():
await asyncio.sleep(1)
print('Hello')
asyncio.run(main())
11.4 Utilizzo di librerie con gestione dei thread propria
Alcune librerie, come NumPy e SciPy, sono scritte in C e utilizzano meccanismi di gestione dei thread propri, permettendo loro di aggirare il GIL e di sfruttare efficacemente i processori multi-core per i calcoli.
In effetti, questa è una delle principali ragioni del successo di Python, per quanto lento possa essere. Tutti i calcoli complessi sono riscritti in linguaggi C/C++ ed eseguiti su tutti i core del processore o addirittura sui core della scheda grafica. E sulle moderne schede grafiche ci sono migliaia di core.
Risulta che non importa più quanto sia veloce o lento il linguaggio, se tutti i calcoli che richiedono molte risorse vengono eseguiti da librerie esterne o addirittura spostati in data center remoti.
11.5 Esecuzione di calcoli al di fuori dell'interprete Python
L'uso di estensioni in C/C++ o altri linguaggi che possono eseguire calcoli al di fuori dell'interprete Python e poi restituire il risultato. Questo permette di evitare di bloccare il GIL durante l'esecuzione di calcoli intensivi.
Esempio di utilizzo di Cython:
# esempio.pyx
def compute(int n):
cdef int i, result = 0
for i in range(n):
result += i
return result
Compilazione e utilizzo:
cythonize -i example.pyx
import example
print(example.compute(1000000))
GO TO FULL VERSION