Corso
Se stai iniziando con Python e vuoi saperne di più, segui il corso di DataCamp Introduzione alla Data Science in Python.
Un global interpreter lock (GIL) è un meccanismo che applica un blocco globale a un interprete. Viene usato negli interpreti dei linguaggi di programmazione per sincronizzare e gestire l'esecuzione dei thread in modo che solo un thread nativo (schedulato dal sistema operativo) possa essere eseguito alla volta.
In uno scenario con più thread, può accadere che i thread cerchino entrambi di acquisire la memoria nello stesso istante e, di conseguenza, sovrascrivano i dati in memoria. Da qui nasce l'esigenza di un meccanismo che aiuti a prevenire questo fenomeno.
Alcuni interpreti noti che hanno il GIL sono CPython e Ruby MRI. Come molti sanno, Python è un linguaggio interpretato e ha diverse distribuzioni come CPython, Jython, IronPython. Tra questi, il GIL è supportato solo in CPython, che è anche l'implementazione di Python più usata. CPython è stato sviluppato sia in C che in Python principalmente per supportare e lavorare con applicazioni che hanno molto C “sotto il cofano”.
Anche se il tuo processore ha più core, un interprete globale consentirà l'esecuzione di un solo thread alla volta. Questo perché, quando un thread inizia a girare, acquisisce il global interpreter lock. Quando attende un'operazione di I/O (lettura/scrittura su disco) o un'operazione legata alla CPU (moltiplicazione vettoriale/matriciale), rilascia il lock così che altri thread di quel processo possano girare. Di conseguenza, non puoi eseguire gli altri thread nello stesso momento.

Prendiamoci un momento per capire il diagramma sopra. Come vedi, c'è una funzione factorial e due thread, 1 e 2: il thread 1 è in stato bloccato mentre il thread 2 è in attesa. Questo significa che solo uno dei thread può accedere alla funzione. Ora, supponiamo che la funzione factorial impieghi 2 secondi per completarsi. In un caso ideale, entrambi i thread dovrebbero riuscire a terminare l'esecuzione in 2 secondi. Tuttavia, in Python non è così: i due thread gireranno in serie e non in parallelo.
I thread 1 e 2 che chiamano la funzione factorial possono impiegare il doppio del tempo rispetto a un singolo thread che chiama la funzione due volte. Questo indica anche che il gestore della memoria governato dall'interprete non è thread-safe, cioè più thread non riescono ad accedere simultaneamente agli stessi dati condivisi.
Quindi, il GIL (Fonte: Understanding the Python GIL):
-
Limita le operazioni con i thread.
-
Restringe l'esecuzione parallela.
-
Garantisce che solo un thread alla volta giri nell'interprete.
-
Aiuta a semplificare vari dettagli di basso livello come la gestione della memoria.
Con il GIL si ottiene un computing cooperativo o multitasking coordinato, invece del computing parallelo.
Come si può osservare dal diagramma qui sopra, ci sono tre thread: inizialmente il thread 1 sta girando e ha acquisito il GIL e, quando viene eseguita un'operazione di I/O come una lettura, una scrittura, ecc., il thread 1 rilascia il GIL che viene poi acquisito dal thread 2. Questo ciclo continua e il GIL continua a passare alternativamente tra i thread finché non hanno completato l'esecuzione del programma. Ricorda che se uno dei thread non ha il lock e non ha terminato l'esecuzione, rimarrà in stato di attesa.
GIL in Python2 vs. Python3

Il Global Interpreter Lock in Python 2.7 funziona in modo diverso rispetto a Python3. In un'operazione puramente CPU-bound, il thread continua a girare poiché non ci sono operazioni di I/O; in tal caso gli altri thread resteranno inattivi o in attesa, cosa indesiderata. Per risolvere questo problema, Python2 usa la terminologia dei ticks. Il global interpreter lock effettua un controllo per monitorare lo stato del thread — ad esempio attesa, operazione di I/O, o se è in esecuzione. Tuttavia, il GIL non monitora i thread a ogni istante; utilizza invece il concetto di ticks e controlla i thread ogni 100 tick.
Un tick è un'istruzione di bytecode e, quando sono completate 100 istruzioni di bytecode, il GIL verifica se i thread stanno girando o sono in attesa. È importante ricordare che un tick non è legato al tempo, poiché è un'istruzione di bytecode. Ogni tick può richiedere più o meno tempo per l'esecuzione rispetto a un altro.
Questo controllo periodico dei thread ogni 100 tick è essenziale, soprattutto nelle operazioni CPU-bound, poiché non prevedono I/O. Nota che puoi modificare il contatore dei tick usando il modulo built-in sys di Python.
Discutiamo alcuni svantaggi dell'approccio sopra:
Se guardi questo diagramma, puoi notare che i thread non girano in parallelo ma in ordine seriale, noto anche come computing cooperativo governato dal global interpreter lock. Inoltre, i thread devono attendere per periodi più lunghi. Per esempio, il thread T3 ha dovuto aspettare che entrambi i thread T1 e T2 rilasciassero il GIL. Quindi, i thread “soffrono” per ottenere il lock e la CPU.
Un altro svantaggio è la competizione tra thread per acquisire il GIL. All'inizio può darsi che, in base alla priorità, T1 ottenga il GIL, poi T2 e così via. Tuttavia, quando T3 rilascia il GIL, invierà una notifica a tutti gli altri thread, e questi continueranno a contendersi l'acquisizione del GIL. Detto ciò, si può potenzialmente mitigare impostando le priorità di ciascun thread.
In Python3, a ogni thread viene assegnato un tempo fisso, cioè 5 ms di tempo di esecuzione. Questo impedisce ai thread di restare a corto di risorse, poiché il tempo di attesa è uguale per tutti. I thread aspetteranno per vedere se il GIL viene rilasciato volontariamente dal Thread 1 a causa di un'operazione di I/O o di sleep. In caso contrario, ne forzeranno il rilascio dopo 5 ms. Di conseguenza, non ci sarà alcuna carenza di tempo CPU, perché ogni thread avrà una quantità di tempo uguale, eliminando così la difficoltà di acquisire il GIL.
Tuttavia, è importante notare che la nuova implementazione del GIL dà più priorità ai job CPU-bound rispetto alle operazioni di I/O. Nel caso in cui un thread abbia rilasciato il GIL e due thread siano in attesa, il GIL verrà acquisito dal thread che deve eseguire un job CPU-bound rispetto all'altro thread.
Conclusione
Complimenti per aver concluso il tutorial.
Questo tutorial ha trattato un argomento avanzato e l'obiettivo era offrirti una panoramica teorica di come funziona esattamente il GIL in Python. Poiché non abbiamo affrontato aspetti di codice relativi a GIL e thread, un buon esercizio per te è sperimentare creando più thread e un metodo che esegua un compito intensivo di I/O, per poi analizzare come il GIL viene passato tra i vari thread.
Se hai domande su questo tutorial, sentiti libero di scriverle nei commenti qui sotto.
Riferimenti:
Se stai iniziando con Python e vuoi saperne di più, segui il corso di DataCamp Introduzione alla Data Science in Python.

