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 Queue
s 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 Queue
s?
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.