Le GIL ou Global Interpreter Lock est un verrou unique auquel l'interpréteur Python fait appel constamment pour protéger tous les objets qu'il manipule contre des accès concurrentiels.
from jyquickhelper import add_notebook_menu
add_notebook_menu()
On mesure le temps nécessaire pour créer deux liste et comparer ce temps avec celui que cela prendrait en parallèle.
def create_list(n):
res = []
for i in range(n):
res.append(i)
return res
%timeit create_list(100000)
10.4 ms ± 1.87 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
En parallèle avec le module concurrent.futures et deux appels à la même fonction.
from concurrent.futures import ThreadPoolExecutor
def run2(nb):
with ThreadPoolExecutor(max_workers=2) as executor:
for res in executor.map(create_list, [nb, nb+1]):
pass
%timeit run2(100000)
54.7 ms ± 4.94 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
C'est plus long que si les calculs étaient lancés les uns après les autres. Ce temps est perdu à synchroniser les deux threads bien que les deux boucles n'aient rien à échanger. Chaque thread passe son temps à attendre que l'autre ait terminé de mettre à jour sa liste et le GIL impose que ces mises à jour aient lieu une après l'autre.
Au lieu de mettre à jour une liste, on va lancer un thread qui ne fait rien qu'attendre. Donc le GIL n'est pas impliqué.
import time
def attendre(t=0.009):
time.sleep(t)
return None
%timeit attendre()
9.36 ms ± 28.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
def run2(t):
with ThreadPoolExecutor(max_workers=2) as executor:
for res in executor.map(attendre, [t, t+0.001]):
pass
%timeit run2(0.009)
12.6 ms ± 43.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Les deux attentes se font en parallèle car le temps moyen est significativement inférieur à la somme des deux attentes.