Les listes sont-elles thread-safe?

155

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?

lémiant
la source
1
Difficile de toujours dire ce qui est exactement garanti thread-safe en Python, et il est difficile de raisonner sur la sécurité des threads. Même le très populaire portefeuille Bitcoin Electrum a eu des bogues de concurrence en résultant probablement.
sudo

Réponses:

182

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.

Thomas Wouters
la source
1
Deque est-il également thread-safe? Cela me semble plus approprié pour mon utilisation.
lemiant
20
Tous les objets Python ont le même type de sécurité des threads - ils ne sont pas eux-mêmes corrompus, mais leurs données le peuvent. collections.deque est ce qui se cache derrière les objets Queue.Queue. Si vous accédez à des éléments à partir de deux threads, vous devriez vraiment utiliser les objets Queue.Queue. Vraiment.
Thomas Wouters
10
lemiant, deque est thread-safe. Extrait du chapitre 2 de Fluent Python: "La classe collections.deque est une file d'attente à deux extrémités thread-safe conçue pour une insertion et une suppression rapides des deux extrémités. [...] Les opérations d'ajout et de popleft sont atomiques, donc deque est sûr de utiliser comme file d'attente LIFO dans les applications multithreads sans avoir besoin d'utiliser des verrous. "
Al Sweigart
3
Cette réponse concerne-t-elle CPython ou Python? Quelle est la réponse pour Python lui-même?
user541686
@Nils: Euh, la première page lié à dit Python au lieu de CPython parce qu'il est décrit le langage Python. Et ce deuxième lien indique littéralement qu'il existe plusieurs implémentations du langage Python, une seule qui se trouve être plus populaire. Étant donné que la question portait sur Python, la réponse devrait décrire ce qui peut être garanti dans toute implémentation conforme de Python, pas seulement ce qui se passe dans CPython en particulier.
user541686
90

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.

dotancohen
la source
1
PyList_Append lit à partir de la mémoire. Voulez-vous dire que ses lectures et écritures se produisent dans le même verrou GIL? github.com/python/cpython/blob/…
amwinter
1
@amwinter Oui, tout l'appel à PyList_Appendest 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_Appendsoit 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 okpeut ê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.
greggo
3
Le point de dotancohen est qu'il L[0] += xeffectuera un __getitem__on L, puis un __setitem__on L- si le Lsupporte, __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 Lniveau de l'interpréteur python (vous les verrez dans le bytecode compilé). Le appendse fait dans un seul appel de méthode dans le bytecode.
greggo
6
Et pourquoi pas remove?
pâturage le
2
voté! alors puis-je ajouter un fil en continu et insérer un autre fil?
PirateApp
2

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

PirateApp
la source
6
La version avec serrures a le même comportement que celle sans serrures. Fondamentalement, l'erreur vient car il tente de supprimer quelque chose qui ne figure pas dans la liste, cela n'a rien à voir avec la sécurité des threads. Essayez d'exécuter la version avec des verrous après avoir changé l'ordre de démarrage, c'est-à-dire démarrez t2 avant t1 et vous verrez la même erreur. chaque fois que t2 devance t1, l'erreur se produit, que vous utilisiez des verrous ou non.
Dev
1
De plus, il vaut mieux utiliser un gestionnaire de contexte ( with r:) au lieu d'appeler explicitement r.acquire()etr.release()
GordonAitchJay
1
@GordonAitchJay 👍
Timothy C. Quinn