Je remarque qu'il est souvent suggéré d'utiliser des files d'attente avec plusieurs threads, au lieu de listes et .pop()
. Est-ce parce que les listes ne sont pas thread-safe ou pour une autre raison?
Je remarque qu'il est souvent suggéré d'utiliser des files d'attente avec plusieurs threads, au lieu de listes et .pop()
. Est-ce parce que les listes ne sont pas thread-safe ou pour une autre raison?
Réponses:
Les listes elles-mêmes sont thread-safe. Dans CPython, le GIL protège contre les accès simultanés à ceux-ci, et d'autres implémentations prennent soin d'utiliser un verrou à granularité fine ou un type de données synchronisé pour leurs implémentations de liste. Cependant, bien que les listes elles - mêmes ne puissent pas être corrompues par des tentatives d'accès simultané, les données des listes ne sont pas protégées. Par exemple:
L[0] += 1
n'est pas garanti d'augmenter réellement L [0] de un si un autre thread fait la même chose, car ce +=
n'est pas une opération atomique. (Très, très peu d'opérations en Python sont en fait atomiques, car la plupart d'entre elles peuvent provoquer l'appel de code Python arbitraire.) Vous devez utiliser des files d'attente car si vous utilisez simplement une liste non protégée, vous pouvez obtenir ou supprimer le mauvais élément en raison de la race conditions.
Pour clarifier un point de l'excellente réponse de Thomas, il convient de mentionner qu'elle append()
est thread-safe.
En effet, rien ne craint que les données en cours de lecture soient au même endroit une fois que nous y écrivons . L' append()
opération ne lit pas les données, elle écrit uniquement les données dans la liste.
PyList_Append
est effectué dans un verrou GIL. Il reçoit une référence à un objet à ajouter. Le contenu de cet objet peut être modifié après son évaluation et avant que l'appel à PyList_Append
soit effectué. Mais ce sera toujours le même objet, et ajouté en toute sécurité (si vous le faites lst.append(x); ok = lst[-1] is x
, cela ok
peut être faux, bien sûr). Le code que vous référencez ne lit pas à partir de l'objet ajouté, sauf pour INCREF. Il lit, et peut réallouer, la liste qui est annexée.
L[0] += x
effectuera un __getitem__
on L
, puis un __setitem__
on L
- si le L
supporte, __iadd__
cela fera les choses un peu différemment au niveau de l'interface objet, mais il y a toujours deux opérations séparées au L
niveau de l'interpréteur python (vous les verrez dans le bytecode compilé). Le append
se fait dans un seul appel de méthode dans le bytecode.
remove
?
Voici une liste complète mais non exhaustive d'exemples d' list
opérations et de savoir si elles sont thread-safe ou non. En espérant obtenir une réponse concernant la obj in a_list
construction du langage ici .
J'ai récemment eu ce cas où je devais ajouter à une liste en continu dans un fil, parcourir les éléments et vérifier si l'élément était prêt, c'était un AsyncResult dans mon cas et le supprimer de la liste uniquement s'il était prêt. Je n'ai trouvé aucun exemple illustrant clairement mon problème Voici un exemple montrant l'ajout à la liste dans un thread en continu et la suppression de la même liste dans un autre thread en continu La version défectueuse fonctionne facilement sur des nombres plus petits mais gardez les nombres suffisamment grands et exécutez un quelques fois et vous verrez l'erreur
La version FLAWED
import threading
import time
# Change this number as you please, bigger numbers will get the error quickly
count = 1000
l = []
def add():
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
Sortie lorsque ERROR
Exception in thread Thread-63:
Traceback (most recent call last):
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "<ipython-input-30-ecfbac1c776f>", line 13, in remove
l.remove(i)
ValueError: list.remove(x): x not in list
Version qui utilise des verrous
import threading
import time
count = 1000
l = []
lock = threading.RLock()
def add():
with lock:
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
with lock:
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
Production
[] # Empty list
Conclusion
Comme mentionné dans les réponses précédentes, alors que le fait d'ajouter ou de faire sauter des éléments de la liste elle-même est thread-safe, ce qui n'est pas thread-safe, c'est lorsque vous ajoutez un thread et en insérez un autre
with r:
) au lieu d'appeler explicitement r.acquire()
etr.release()