Python Language
Lavorare attorno al Global Interpreter Lock (GIL)
Ricerca…
Osservazioni
Perché c'è un GIL?
La GIL è stata utilizzata in CPython sin dall'inizio dei thread Python, nel 1992. È progettata per garantire la sicurezza dei thread nell'esecuzione del codice Python. Gli interpreti Python scritti con un GIL impediscono a più thread nativi di eseguire contemporaneamente bytecode Python. Ciò semplifica i plug-in per garantire che il loro codice sia sicuro per i thread: basta bloccare GIL e solo il thread attivo può essere eseguito, quindi il codice è automaticamente thread-safe.Versione breve: GIL garantisce che, indipendentemente dal numero di processori e thread, verrà eseguito solo un thread di un interprete Python alla volta.
Questo ha molti benefici di facilità d'uso, ma ha anche molti benefici negativi.
Nota che un GIL non è un requirment del linguaggio Python. Di conseguenza, non puoi accedere a GIL direttamente dal codice Python standard. Non tutte le implementazioni di Python usano un GIL.
Interpreti che hanno un GIL: CPython, PyPy, Cython (ma puoi disabilitare GIL con nogil
)
Interpreti che non hanno un GIL: Jython, IronPython
Dettagli su come funziona GIL:
Quando un thread è in esecuzione, blocca GIL. Quando un thread vuole essere eseguito, richiede GIL e attende finché non è disponibile. In CPython, prima della versione 3.2, il thread in esecuzione controllava dopo un certo numero di istruzioni python per vedere se altro codice voleva il lock (cioè, rilasciava il lock e poi lo richiedeva nuovamente). Questo metodo tendeva a causare l'inattività del thread, in gran parte perché il thread che rilasciava il blocco lo acquisiva nuovamente prima che i thread in attesa avessero la possibilità di riattivarsi. Dalla versione 3.2, i thread che vogliono che il GIL attenda il blocco per un po 'di tempo e, dopo questo periodo, impostano una variabile condivisa che impone il rendimento del thread in esecuzione. Ciò può comunque comportare tempi di esecuzione drasticamente più lunghi. Vedi i collegamenti sottostanti da dabeaz.com (nella sezione dei riferimenti) per maggiori dettagli.CPython rilascia automaticamente GIL quando un thread esegue un'operazione di I / O. Le librerie di elaborazione delle immagini e le operazioni di crunch numerico rilasciano GIL prima di elaborarle.
Benefici della GIL
Per gli interpreti che usano GIL, GIL è sistemico. È usato per preservare lo stato dell'applicazione. I vantaggi includono:- Garbage Collection: i conteggi dei riferimenti a livello di thread devono essere modificati mentre GIL è bloccato. In CPython, tutta la collezione di garbarge è legata a GIL. Questo è un grande; vedi l'articolo wiki di python.org sul GIL (elencato in Riferimenti, sotto) per i dettagli su cosa deve ancora essere funzionale se si volesse rimuovere GIL.
- Facilità per i programmatori che si occupano di GIL - il blocco di tutto è semplicistico, ma facile da codificare
- Facilita l'importazione di moduli da altre lingue
Conseguenze del GIL
GIL consente solo a un thread di eseguire codice Python alla volta all'interno dell'interprete python. Ciò significa che il multithreading di processi che eseguono rigorosi codici Python semplicemente non funziona. Quando si utilizzano i thread contro GIL, è probabile che si ottengano prestazioni peggiori con i thread rispetto a quando si esegue una singola discussione.Riferimenti:
https://wiki.python.org/moin/GlobalInterpreterLock - riepilogo rapido di ciò che fa, dettagli precisi su tutti i vantaggi
http://programmers.stackexchange.com/questions/186889/why-was-python-written-with-the-gil - riassunto scritto in modo chiaro
http://www.dabeaz.com/python/UnderstandingGIL.pdf - come funziona GIL e perché rallenta su più core
http://www.dabeaz.com/GIL/gilvis/index.html - visualizzazione dei dati che mostrano come GIL blocca i thread
http://jeffknupp.com/blog/2012/03/31/pythons-hardest-problem/ - storia semplice da capire del problema GIL
https://jeffknupp.com/blog/2013/06/30/pythons-hardest-problem-revisited/ - dettagli sui modi per aggirare i limiti del GIL
Multiprocessing.Pool
La semplice risposta, quando si chiede come utilizzare i thread in Python è: "Non usare i processi, invece". Il modulo multiprocessing consente di creare processi con sintassi simile alla creazione di thread, ma preferisco utilizzare il loro conveniente oggetto Pool.
Usando il codice che David Beazley ha usato per mostrare i pericoli dei thread contro il GIL , lo riscriviamo usando multiprocessing.Pool :
Il codice di David Beazley che mostrava problemi di threading GIL
from threading import Thread
import time
def countdown(n):
while n > 0:
n -= 1
COUNT = 10000000
t1 = Thread(target=countdown,args=(COUNT/2,))
t2 = Thread(target=countdown,args=(COUNT/2,))
start = time.time()
t1.start();t2.start()
t1.join();t2.join()
end = time.time()
print end-start
Riscritto usando multiprocessing.Pool: import multiprocessing
import time
def countdown(n):
while n > 0:
n -= 1
COUNT = 10000000
start = time.time()
with multiprocessing.Pool as pool:
pool.map(countdown, [COUNT/2, COUNT/2])
pool.close()
pool.join()
end = time.time()
print(end-start)
Invece di creare thread, questo crea nuovi processi. Poiché ogni processo è il suo interprete, non ci sono collisioni GIL. multiprocessing.Pool aprirà tanti processi quanti sono i core sulla macchina, sebbene nell'esempio sopra, ne avrebbero solo bisogno due. In uno scenario del mondo reale, si desidera progettare il proprio elenco in modo che abbia una lunghezza pari a quella dei processori sulla macchina. Il Pool eseguirà la funzione che gli dici di eseguire con ogni argomento, fino al numero di processi che crea. Al termine della funzione, tutte le restanti funzioni nell'elenco verranno eseguite su tale processo.
Ho scoperto che, anche usando l'istruzione with
, se non si chiude e si aggiunge al pool, i processi continuano ad esistere. Per ripulire le risorse, chiudo sempre e mi unisco ai miei pool.
Cython nogil:
Cython è un interprete python alternativo. Usa GIL, ma ti consente di disabilitarlo. Vedi la loro documentazione
Ad esempio, usando il codice che David Beazley ha usato per mostrare i pericoli dei thread contro il GIL , lo riscriviamo usando nogil:
Il codice di David Beazley che mostrava problemi di threading GIL
from threading import Thread
import time
def countdown(n):
while n > 0:
n -= 1
COUNT = 10000000
t1 = Thread(target=countdown,args=(COUNT/2,))
t2 = Thread(target=countdown,args=(COUNT/2,))
start = time.time()
t1.start();t2.start()
t1.join();t2.join()
end = time.time()
print end-start
Riscritto usando nogil (SOLO FUNZIONA IN CYTHON):
from threading import Thread
import time
def countdown(n):
while n > 0:
n -= 1
COUNT = 10000000
with nogil:
t1 = Thread(target=countdown,args=(COUNT/2,))
t2 = Thread(target=countdown,args=(COUNT/2,))
start = time.time()
t1.start();t2.start()
t1.join();t2.join()
end = time.time()
print end-start
È così semplice, finché usi cython. Nota che la documentazione dice che devi assicurarti di non cambiare alcun oggetto python:
Il codice nel corpo dell'istruzione non deve manipolare oggetti Python in alcun modo e non deve chiamare nulla che manipoli oggetti Python senza prima re-acquisire il GIL. Attualmente Cython non controlla questo.