Les verrous sont-ils inutiles dans le code Python multithread à cause du GIL?

Si vous comptez sur une implémentation de Python qui a un Global Interpreter Lock (c.-à-d. CPython) et qui écrit du code multithread, avez-vous vraiment besoin de verrous?

Si le GIL ne permet pas l’exécution simultanée de plusieurs instructions, les données partagées ne seraient-elles pas inutiles à protéger?

désolé si c’est une question stupide, mais c’est quelque chose que je me suis toujours demandé sur Python sur les machines multiprocesseurs / core.

La même chose s’appliquerait à toute autre implémentation de langue ayant un GIL.

Vous aurez toujours besoin de verrous si vous partagez un état entre les threads. Le GIL protège uniquement l’interprète en interne. Vous pouvez toujours avoir des mises à jour incohérentes dans votre propre code.

Par exemple:

#!/usr/bin/env python import threading shared_balance = 0 class Deposit(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance balance = shared_balance balance += 100 shared_balance = balance class Withdraw(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance balance = shared_balance balance -= 100 shared_balance = balance threads = [Deposit(), Withdraw()] for thread in threads: thread.start() for thread in threads: thread.join() print shared_balance 

Ici, votre code peut être interrompu entre la lecture de l’état partagé ( balance = shared_balance ) et l’écriture du résultat modifié ( shared_balance = balance ), entraînant une perte de mise à jour. Le résultat est une valeur aléatoire pour l’état partagé.

Pour que les mises à jour soient cohérentes, les méthodes d’exécution doivent verrouiller l’état partagé autour des sections read-modify-write (à l’intérieur des boucles) ou trouver un moyen de détecter l’évolution de l’état partagé depuis sa lecture .

Non – le GIL protège simplement les internes python de plusieurs threads modifiant leur état. C’est un très faible niveau de locking, suffisant uniquement pour maintenir les propres structures de python dans un état cohérent. Il ne couvre pas le locking au niveau de l’ application que vous devrez effectuer pour couvrir la sécurité des threads dans votre propre code.

L’essence du locking est de s’assurer qu’un bloc de code particulier n’est exécuté que par un thread. Le GIL applique cela pour les blocs de la taille d’un seul bytecode, mais généralement vous voulez que le verrou couvre un bloc de code plus grand que celui-ci.

Ajout à la discussion:

Comme le GIL existe, certaines opérations sont atomiques en Python et n’ont pas besoin de verrou.

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

Comme indiqué par les autres réponses, vous devez néanmoins utiliser des verrous chaque fois que la logique de l’application l’exige (par exemple, dans un problème Producer / Consumer).

Le Global Interpreter Lock empêche les threads d’accéder à l’ interpréteur simultanément (ainsi, CPython n’utilise qu’un seul cœur). Cependant, si je comprends bien, les threads sont toujours interrompus et programmés de manière préventive , ce qui signifie que vous avez toujours besoin de verrous sur les structures de données partagées, de peur que vos threads ne se gênent mutuellement.

La réponse que j’ai rencontrée à maintes resockets est que le multithreading en Python vaut rarement la peine, à cause de cela. J’ai entendu de bonnes choses sur le projet PyProcessing , qui rend l’exécution de plusieurs processus aussi simples que le multithreading, avec des structures de données partagées, des files d’attente, etc. (PyProcessing sera intégré au module Python 2.6). .) Cela vous permet de faire le tour du GIL, chaque processus ayant son propre interprète.

Ce post décrit le GIL à un niveau assez élevé:

Ces citations présentent un intérêt particulier:

Toutes les dix instructions (cette valeur par défaut peut être modifiée), le kernel libère le GIL pour le thread en cours. À ce stade, le système d’exploitation choisit un thread parmi tous les threads en compétition pour le verrou (en choisissant éventuellement le même thread qui vient de lancer le GIL – vous n’avez aucun contrôle sur le thread choisi); ce thread acquiert le GIL et s’exécute ensuite pour dix autres bytecodes.

et

Notez bien que le GIL restreint uniquement le code Python pur. Des extensions (bibliothèques Python externes généralement écrites en C) peuvent être écrites pour libérer le verrou, ce qui permet ensuite à l’interpréteur Python de s’exécuter séparément de l’extension jusqu’à ce que l’extension réacquiert le verrou.

Il semblerait que le GIL ne fournisse que peu d’instances possibles pour un changement de contexte et que les systèmes multi-cœurs / processeurs se comportent comme un seul cœur, par rapport à chaque instance d’interpréteur python.

Pense-y de cette façon:

Sur un ordinateur à processeur unique, le multithreading se produit en suspendant un thread et en démarrant un autre assez rapidement pour le faire apparaître en même temps. C’est comme Python avec le GIL: un seul thread est en cours d’exécution.

Le problème est que le thread peut être suspendu n’importe où, par exemple, si je veux calculer b = (a + b) * 3, cela pourrait produire des instructions comme ceci:

 1 a += b 2 a *= 3 3 b = a 

Maintenant, supposons que ce processus s’exécute dans un thread et que ce thread est suspendu après la ligne 1 ou 2, puis qu’un autre thread se lance et s’exécute:

 b = 5 

Ensuite, lorsque l’autre thread reprend, b est remplacé par les anciennes valeurs calculées, ce qui n’est probablement pas ce à quoi on s’attendait.

Donc, vous pouvez voir que même s’ils ne sont PAS en cours d’exécution en même temps, vous avez toujours besoin d’un locking.

Vous devez toujours utiliser des verrous (votre code peut être interrompu à tout moment pour exécuter un autre thread et cela peut entraîner des incohérences dans les données). Le problème avec GIL est qu’il empêche le code Python d’utiliser plus de cœurs en même temps (ou plusieurs processeurs s’ils sont disponibles).

Les serrures sont toujours nécessaires. Je vais essayer d’expliquer pourquoi ils sont nécessaires.

Toute opération / instruction est exécutée dans l’interpréteur. GIL garantit que l’interprète est tenu par un seul thread à un instant donné . Et votre programme avec plusieurs threads fonctionne dans un seul interpréteur. À un instant donné, cet interpréteur est tenu par un seul thread. Cela signifie que seul le thread qui contient l’interpréteur est en cours d’exécution à tout moment.

Supposons qu’il y ait deux threads, disons t1 et t2, et que les deux veulent exécuter deux instructions qui lisent la valeur d’une variable globale et l’incrémentent.

 #increment value global var read_var = var var = read_var + 1 

Comme indiqué ci-dessus, GIL garantit uniquement que deux threads ne peuvent pas exécuter une instruction simultanément, ce qui signifie que les deux threads ne peuvent pas exécuter read_var = var à un instant donné. Mais ils peuvent exécuter des instructions les unes après les autres et vous pouvez toujours avoir des problèmes. Considérez cette situation:

  • Supposons que read_var est 0.
  • GIL est tenu par le thread t1.
  • t1 exécute read_var = var . Donc, read_var dans t1 est 0. GIL s’assurera que cette opération de lecture ne sera exécutée pour aucun autre thread à cet instant.
  • GIL est donné au thread t2.
  • t2 exécute read_var = var . Mais read_var est toujours 0. Donc, read_var dans t2 est 0.
  • GIL est donné à t1.
  • t1 exécute var = read_var+1 et var devient 1.
  • GIL est donné à t2.
  • t2 pense read_var = 0, parce que c’est ce qu’il lit.
  • t2 exécute var = read_var+1 et var devient 1.
  • Nous nous attendions à ce que var devienne 2.
  • Ainsi, un verrou doit être utilisé pour continuer à lire et à incrémenter comme une opération atomique.
  • La réponse de Will Harris l’explique par un exemple de code.

Un peu de mise à jour de l’exemple de Will Harris:

 class Withdraw(threading.Thread): def run(self): for _ in xrange(1000000): global shared_balance if shared_balance >= 100: balance = shared_balance balance -= 100 shared_balance = balance 

Mettez une déclaration de vérification de valeur dans le retrait et je ne vois plus de négatif et les mises à jour semblent cohérentes. Ma question est:

Si GIL empêche qu’un seul thread puisse être exécuté à n’importe quelle heure atomique, alors où serait la valeur périmée? Si aucune valeur périmée, pourquoi avons-nous besoin de verrouiller? (En supposant que nous ne parlons que du code python pur)

Si je comprends bien, la vérification des conditions ci-dessus ne fonctionnerait pas dans un environnement de threading réel . Lorsque plusieurs threads s’exécutent simultanément, une valeur obsolète peut être créée, d’où l’incohérence de l’état du partage. Vous avez alors vraiment besoin d’un verrou. Mais si python n’autorise vraiment qu’un seul thread à la fois (le découpage en tranches de temps), il ne devrait pas être possible d’exister une valeur périmée, non?