Le GIL#

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.

Deux listes en parallel#

On mesure le temps nécessaire pour créer deux liste et comparer ce temps avec celui que cela prendrait en parallèle.

import timeit
import time
from concurrent.futures import ThreadPoolExecutor


def create_list(n):
    res = []
    for i in range(n):
        res.append(i)
    return res


timeit.timeit("create_list(100000)", globals=globals(), number=100)
0.5017731000007188

En parallèle avec le module concurrent.futures et deux appels à la même fonction.

def run2(nb):
    with ThreadPoolExecutor(max_workers=2) as executor:
        for res in executor.map(create_list, [nb, nb + 1]):
            pass


timeit.timeit("run2(100000)", globals=globals(), number=100)
1.0972130999998626

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é.

def attendre(t=0.009):
    time.sleep(t)
    return None


timeit.timeit("attendre()", globals=globals(), number=100)
0.9519763000007515
def run3(t):
    with ThreadPoolExecutor(max_workers=2) as executor:
        for res in executor.map(attendre, [t, t + 0.001]):
            pass


timeit.timeit("run3(0.009)", globals=globals(), number=100)
1.3304714000005333

Les deux attentes se font en parallèle car le temps moyen est significativement inférieur à la somme des deux attentes.

Total running time of the script: (0 minutes 3.887 seconds)

Gallery generated by Sphinx-Gallery