Czy listy są bezpieczne dla wątków?

155

Zauważyłem, że często sugeruje się używanie kolejek z wieloma wątkami zamiast list i .pop(). Czy dzieje się tak, ponieważ listy nie są bezpieczne dla wątków lub z innego powodu?

lemiant
źródło
1
Trudno powiedzieć zawsze, co dokładnie jest gwarantowane jako bezpieczne wątkowo w Pythonie i trudno jest uzasadnić w tym bezpieczeństwo wątków. Nawet bardzo popularny portfel Bitcoin Electrum miał prawdopodobnie wynikające z tego błędy współbieżności.
sudo

Odpowiedzi:

182

Same listy są bezpieczne dla wątków. W CPythonie GIL chroni przed równoczesnym dostępem do nich, a inne implementacje dbają o użycie precyzyjnej blokady lub zsynchronizowanego typu danych dla ich implementacji list. Jednakże, podczas gdy list sami nie mogą przejść przez uszkodzony prób do równoczesnego dostępu listach męska dane nie są chronione. Na przykład:

L[0] += 1

nie ma gwarancji, że faktycznie zwiększy L [0] o jeden, jeśli inny wątek zrobi to samo, ponieważ +=nie jest to operacja atomowa. (Bardzo, bardzo niewiele operacji w Pythonie jest w rzeczywistości atomowych, ponieważ większość z nich może spowodować wywołanie dowolnego kodu w Pythonie). Powinieneś używać kolejek, ponieważ jeśli używasz tylko niezabezpieczonej listy, możesz uzyskać lub usunąć niewłaściwy element z powodu rasy warunki.

Thomas Wouters
źródło
1
Czy deque jest również bezpieczny dla wątków? Wydaje się bardziej odpowiednie do mojego użytku.
lemiant
20
Wszystkie obiekty Pythona mają ten sam rodzaj bezpieczeństwa wątków - same nie ulegają uszkodzeniu, ale ich dane mogą. collections.deque kryje się za obiektami Queue.Queue. Jeśli uzyskujesz dostęp do rzeczy z dwóch wątków, naprawdę powinieneś używać obiektów Queue.Queue. Naprawdę.
Thomas Wouters,
10
lemiant, deque jest bezpieczny dla wątków. Z rozdziału 2 Fluent Python: „Klasa collections.deque to bezpieczna wątkowo podwójna kolejka zaprojektowana do szybkiego wstawiania i usuwania z obu końców. [...] Operacje dołączania i popleft są atomowe, więc deque jest bezpieczna używać jako kolejki LIFO w aplikacjach wielowątkowych bez konieczności używania blokad. "
Al Sweigart,
3
Czy ta odpowiedź dotyczy CPythona czy Pythona? Jaka jest odpowiedź na sam Python?
user541686
@Nils: Uh, pierwsza strona, którą powiązana mówi Python zamiast CPython ponieważ jest opisywania języka Python. Drugi odsyłacz dosłownie mówi, że istnieje wiele implementacji języka Python, z których tylko jedna jest bardziej popularna. Biorąc pod uwagę, że pytanie dotyczyło języka Python, odpowiedź powinna opisywać, co można zagwarantować w dowolnej implementacji zgodnej z Pythonem, a nie tylko to, co dzieje się w szczególności w CPythonie.
user541686
89

Aby wyjaśnić punkt w doskonałej odpowiedzi Thomasa, należy wspomnieć, że append() jest bezpieczny dla wątków.

Dzieje się tak, ponieważ nie ma obaw, że odczytywane dane będą w tym samym miejscu, gdy zaczniemy je pisać . append()Operacja nie odczytuje dane, to tylko zapisuje je na liście.

dotancohen
źródło
1
PyList_Append czyta z pamięci. Czy masz na myśli, że jego odczyty i zapisy odbywają się w tym samym zamku GIL? github.com/python/cpython/blob/…
amwinter
1
@amwinter Tak, całe wywołanie PyList_Appendodbywa się w jednym zamku GIL. Otrzymuje odniesienie do obiektu do dołączenia. Zawartość tego obiektu może ulec zmianie po jego ocenie i przed wykonaniem wywołania PyList_Append. Ale nadal będzie to ten sam obiekt i bezpiecznie dołączony (jeśli to zrobisz lst.append(x); ok = lst[-1] is x, okmoże to być oczywiście Fałsz). Kod, do którego się odwołujesz, nie czyta z dołączonego obiektu, z wyjątkiem jego ZWIĘKSZENIA. Czyta listę, do której jest dołączona, i może ją ponownie przydzielić.
greggo
3
punkt dotancohen „s jest to, że L[0] += xwykona __getitem__on La potem __setitem__na L- jeśli Lpodpory __iadd__będzie robić rzeczy trochę inaczej na styku obiektu, ale nadal istnieją dwie odrębne operacje na Lna poziomie interpreter Pythona (widać je w kompilowany kod bajtowy). appendOdbywa się aa pojedynczego wywołania metody w kodu bajtowego.
greggo
6
A co powiesz remove?
walka
2
przegłosowano! więc czy mogę dołączyć do jednego wątku w sposób ciągły i wstawić do innego wątku?
PirateApp
2

Niedawno miałem taki przypadek, w którym musiałem stale dołączać do listy w jednym wątku, przeglądać elementy w pętli i sprawdzać, czy element był gotowy, w moim przypadku był to AsyncResult i usunąć go z listy tylko wtedy, gdy był gotowy. Nie mogłem znaleźć żadnych przykładów, które jasno pokazałyby mój problem Oto przykład pokazujący ciągłe dodawanie do listy w jednym wątku i ciągłe usuwanie z tej samej listy w innym wątku. Wadliwa wersja działa łatwo na mniejszych liczbach, ale utrzymuj liczby wystarczająco duże i uruchom kilka razy, a zobaczysz błąd

Wersja USZKODZONA

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)

Wyjście, gdy 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

Wersja wykorzystująca zamki

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)

Wynik

[] # Empty list

Wniosek

Jak wspomniano we wcześniejszych odpowiedziach, podczas gdy czynność dołączania lub wyskakiwania elementów z samej listy jest bezpieczna dla wątków, to, co nie jest bezpieczne dla wątków, to dołączanie do jednego wątku i wstawianie do innego

PirateApp
źródło
6
Wersja z zamkami zachowuje się tak samo jak wersja bez zamków. Zasadniczo błąd nadchodzi, ponieważ próbuje usunąć coś, czego nie ma na liście, nie ma to nic wspólnego z bezpieczeństwem wątków. Spróbuj uruchomić wersję z blokadami po zmianie kolejności początkowej, tj. Rozpocznij t2 przed t1, a zobaczysz ten sam błąd. ilekroć t2 wyprzedza t1, błąd wystąpi bez względu na to, czy używasz blokad, czy nie.
Dev,
1
Poza tym lepiej jest używać menedżera kontekstu ( with r:) zamiast jawnie dzwonić r.acquire()ir.release()
GordonAitchJay
1
@GordonAitchJay 👍
Timothy C. Quinn