web-dev-qa-db-fra.com

Comment les files d'attente sont-elles rendues thread-safe dans Python

J'avoue que cela m'a été demandé lors d'une interview il y a longtemps, mais je n'ai jamais pris la peine de le vérifier.

La question était simple, comment Python rend Queue thread-safe?

Ma réponse a été, en raison du verrouillage d'interpréteur (GIL), chaque fois qu'un seul thread passe un appel pour obtenir l'élément de la file d'attente pendant que d'autres dorment/attendent. Je n'étais/ne suis toujours pas sûr que ce soit une réponse valide.

L'enquêteur semblait insatisfait et a demandé si Queues sont thread-safe dans Java ou implémentation .Net de Python qui n'a pas GIL? Si oui, comment font-ils implémenter une fonctionnalité thread-safe sur ladite structure de données.

J'ai essayé de le chercher, mais je semble toujours tomber sur how to use thread-safe queues.

Alors, comment Queue ou un simple list peut-il être rendu thread-safe et éviter les conditions de concurrence?

Sinon, quels algorithmes ou techniques sont utilisés par GEvent implémentation de thread-safe Queues?

6
Harsh Gupta

Vous vous trompez que le GIL rendrait un programme Python threadsafe. Il ne fait que l'interpréteur lui-même threadsafe.

Par exemple, regardons une file d'attente super simple LIFO (aka. Une pile). Nous ignorerons qu'un list peut déjà être utilisé comme une pile.

class Stack(object):
  def __init__(self, capacity):
    self.size = 0
    self.storage = [None] * capacity

  def Push(self, value):
    self.storage[self.size] = value
    self.size += 1

  def pop(self):
    self.size -= 1
    result = self.storage[self.size]
    self.storage[self.size] = None
    return result

Est-ce sûr pour les threads? Absolument pas, malgré le passage sous le GIL.

Considérez cette séquence d'événements:

  • Le thread 1 ajoute quelques valeurs

    stack = Stack(5)
    stack.Push(1)
    stack.Push(2)
    stack.Push(3)
    

    L'état est maintenant storage=[1, 2, 3, None, None], size=3.

  • Le thread 1 ajoute une valeur stack.Push(4) et est suspendu avant que la taille puisse être incrémentée

    self.storage[self.size] = value
    # interrupted here
    self.size += 1
    

    L'état est maintenant storage=[1, 2, 3, 4, None], size=3.

  • Le thread 2 supprime une valeur stack.pop() qui est 3.

    L'état est maintenant storage=[1, 2, None, 4, None], size=2.

  • Le fil 1 est repris

    self.storage[self.size] = value
    # resume here
    self.size += 1
    

    L'état est maintenant storage=[1, 2, None, 4, None], size=3.

En conséquence, la pile est corrompue: la valeur poussée ne peut pas être récupérée et l'élément supérieur est vide.

Le GIL ne linéarise que les accès aux données, mais cela est presque complètement inutile au développeur ordinaire Python car l'ordre des opérations est encore imprévisible. C'est-à-dire que le GIL ne peut pas être utilisé comme un verrou de niveau Python, il garantit simplement que les valeurs de toutes les variables sont à jour (volatile en C ou Java). Python sans GIL doivent également fournir cette propriété pour la compatibilité, par exemple en utilisant volatile accès à la mémoire ou en utilisant leurs propres verrous. Jython est une implémentation sans GIL qui utilise spécifiquement des implémentations threadsafe pour dict, list, etc. sur.

Parce que Python ne garantit aucun ordre d'opérations entre les threads, il n'est pas surprenant que les structures de données thread-safe doivent utiliser un verrou. Par exemple, la bibliothèque standard queue.Queue Class @ v3.6.4 a un membre mutex et quelques condvars utilisant ce mutex. Tous les accès aux données sont correctement protégés. Mais notez que cette classe n'est pas principalement destinée à une structure de données de file d'attente , mais comme une file d'attente de travaux entre plusieurs threads. Une structure de données pure ne serait généralement pas concernée par le verrouillage.

Bien sûr, les verrous et les mutex puent pour diverses raisons, par exemple en raison de la possibilité de blocages et parce que l'acquisition d'un verrou est lente. En conséquence, il y a beaucoup d'intérêt pour structures de données sans verrouillage . Lorsque le matériel fournit certaines instructions atomiques, il est possible de mettre à jour une structure de données avec une telle opération atomique, par ex. en remplaçant un pointeur. Mais cela a tendance à être assez difficile à faire.

6
amon