As listas são seguras para threads?

155

Percebo que geralmente é sugerido o uso de filas com vários threads, em vez de listas e .pop(). Isso ocorre porque as listas não são seguras para threads, ou por algum outro motivo?

lemiant
fonte
1
Difícil dizer sempre o que exatamente é seguro para threads em Python e é difícil argumentar sobre a segurança de threads nele. Até a altamente popular carteira Bitcoin, a Electrum, teve bugs de concorrência provavelmente decorrentes disso.
Sudo

Respostas:

182

As próprias listas são seguras para threads. No CPython, o GIL protege contra acessos simultâneos a eles, e outras implementações tomam cuidado para usar um bloqueio refinado ou um tipo de dados sincronizado para suas implementações de lista. No entanto, enquanto as próprias listas não podem ficar corrompidas por tentativas de acesso simultâneo, os dados da lista não são protegidos. Por exemplo:

L[0] += 1

não é garantido que realmente aumente L [0] em um se outro thread fizer a mesma coisa, porque +=não é uma operação atômica. (Muito, muito poucas operações no Python são realmente atômicas, porque a maioria delas pode fazer com que o código arbitrário do Python seja chamado.) Você deve usar as Filas porque, se usar apenas uma lista desprotegida, poderá obter ou excluir o item errado devido à raça condições.

Thomas Wouters
fonte
1
O deque também é seguro para threads? Parece mais apropriado para o meu uso.
lemiant
20
Todos os objetos Python têm o mesmo tipo de segurança de thread - eles mesmos não ficam corrompidos, mas seus dados podem. collections.deque é o que está por trás dos objetos Queue.Queue. Se você estiver acessando coisas de dois threads, realmente deve usar objetos Queue.Queue. Realmente.
Thomas Wouters
10
o deque é seguro para threads. No capítulo 2 do Fluent Python: "A classe collections.deque é uma fila de extremidade dupla segura para thread, projetada para inserção e remoção rápidas de ambas as extremidades. [...] As operações de acréscimo e remoção de popleft são atômicas, portanto, o deque é seguro para use como uma fila LIFO em aplicativos multithread sem a necessidade de usar bloqueios. "
Al Sweigart
3
Esta resposta é sobre CPython ou sobre Python? Qual é a resposta para o próprio Python?
user541686
@ Nils: Uh, a primeira página que você vinculou diz Python em vez de CPython, porque está descrevendo a linguagem Python. E esse segundo link literalmente diz que há várias implementações da linguagem Python, apenas uma que é mais popular. Dada a pergunta sobre o Python, a resposta deve descrever o que pode ser garantido em qualquer implementação conforme do Python, não apenas o que acontece no CPython em particular.
User541686
89

Para esclarecer um ponto da excelente resposta de Thomas, deve-se mencionar que append() é seguro para threads.

Isso ocorre porque não há preocupação de que os dados que estão sendo lidos estejam no mesmo local quando formos gravá -los. A append()operação não lê dados, apenas grava dados na lista.

dotancohen
fonte
1
PyList_Append está lendo da memória. Você quer dizer que suas leituras e gravações acontecem no mesmo bloqueio GIL? github.com/python/cpython/blob/…
amwinter
1
@amwinter Sim, toda a chamada PyList_Appendé feita em um bloqueio GIL. É dada uma referência a um objeto para acrescentar. O conteúdo desse objeto pode ser alterado depois de avaliado e antes de a chamada PyList_Append. Mas ainda será o mesmo objeto e anexado com segurança (se você o fizer lst.append(x); ok = lst[-1] is x, okpode ser falso, é claro). O código que você referencia não lê do objeto anexado, exceto para INCREF. Ele lê e pode realocar a lista anexada.
Greggo
3
O ponto de pontoancohen é que L[0] += xele executará um __getitem__on Le depois __setitem__on L- se o Lsuporte __iadd__fizer as coisas de maneira um pouco diferente na interface do objeto, mas ainda existem duas operações separadas no Lnível do interpretador python (você as verá no bytecode compilado). Isso appendé feito em uma chamada de método único no bytecode.
Greggo # 9/16
6
Que tal remove?
precisa saber é o seguinte
2
votado! posso acrescentar um segmento continuamente e inserir outro segmento?
PirateApp 6/04/19
2

Recentemente, tive este caso em que precisava anexar a uma lista continuamente em um thread, percorrer os itens e verificar se o item estava pronto; era um AsyncResult no meu caso e removê-lo da lista apenas se estivesse pronto. Não foi possível encontrar nenhum exemplo que demonstrasse meu problema claramente. Aqui está um exemplo demonstrando a adição à lista em um thread continuamente e a remoção da mesma lista em outro thread continuamente. A versão defeituosa roda facilmente em números menores, mas mantém os números grandes o suficiente e executa uma algumas vezes e você verá o erro

A versão 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)

Saída quando ERRO

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

Versão que usa bloqueios

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)

Resultado

[] # Empty list

Conclusão

Conforme mencionado nas respostas anteriores, enquanto o ato de acrescentar ou estourar elementos da lista em si é seguro para threads, o que não é seguro para threads é quando você acrescenta um thread e aparece em outro

PirateApp
fonte
6
A versão com bloqueios tem o mesmo comportamento que a versão sem bloqueios. Basicamente, o erro está chegando porque ele está tentando remover algo que não está na lista, não tem nada a ver com a segurança do thread. Tente executar a versão com bloqueios após alterar a ordem de início, ou seja, inicie T2 antes de T1 e você verá o mesmo erro. sempre que t2 chegar à frente de t1, o erro ocorrerá, independentemente de você usar bloqueios ou não.
Dev
1
Além disso, é melhor usar um gerenciador de contexto ( with r:) em vez de chamar explicitamente r.acquire()er.release()
GordonAitchJay
1
@GordonAitchJay 👍
Timothy C. Quinn