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?
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?
Respostas:
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.
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.
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
, ok
pode 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.
L[0] += x
ele executará um __getitem__
on L
e depois __setitem__
on L
- se o L
suporte __iadd__
fizer as coisas de maneira um pouco diferente na interface do objeto, mas ainda existem duas operações separadas no L
nível do interpretador python (você as verá no bytecode compilado). Isso append
é feito em uma chamada de método único no bytecode.
remove
?
Aqui está uma lista abrangente e não exaustiva de exemplos de list
operações e se são ou não seguros para threads. Esperando obter uma resposta sobre a obj in a_list
construção da linguagem aqui .
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
with r:
) em vez de chamar explicitamente r.acquire()
er.release()