Le GIL#

Links: notebook, html, PDF, python, slides, GitHub

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()

Deux listes en parallèlle#

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.

Un autre scénario#

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.