Estou trabalhando em um script python3 que manipula sinais (por exemplo, signal.SIGWINCH
) colocando-os em uma fila. Uma thread separada coloca a entrada do usuário na mesma fila, que é toda processada pela thread principal do meu programa.
Ocasionalmente, o programa inteiro trava repentinamente. Identifiquei a causa como sendo a interação entre os manipuladores de sinais e a queue.Queue
classe. Após um pequeno número aleatório de sinais ser enfileirado, o manipulador de sinais bloqueia a fila ao tentar inserir um item nela. Qualquer thread que tente interagir com a fila de qualquer forma (por exemplo, chamando queue.put(block=False)
, queue.get(block=False)
ou queue.empty()
) também trava.
Por que os manipuladores de sinais bloqueiam minha fila, mesmo quando uso funções não bloqueantes? Existe uma maneira segura de adicionar sinais a uma fila em um programa multithread?
Isso pode ser reproduzido executando o trecho de código simplificado abaixo, enquanto redimensiona repetidamente o terminal para acionar o evento (testado em python 3.13, linux):
from queue import Queue, Empty
import signal
event_queue = Queue()
def signal_handler(signum, frame):
event_queue.put(signum)
signal.signal(signal.SIGWINCH, signal_handler)
while True:
try:
print("Attempting to get an event from the queue...")
evt = event_queue.get(block=False)
print("Successfully got event from the queue.")
except Empty:
print("The queue is empty, try again.")
Eventualmente, após redimensionar a janela um número indeterminado de vezes, o código trava após a linha "Tentando obter um evento da fila..."; ou seja, trava em queue.get()
. Mas não deveria, já que especifiquei block=False
. Também tentei usar event_queue.put(signum, block=False)
no meu manipulador de sinais dentro de um try/except, mas ainda trava.
(...)
Attempting to get an event from the queue...
The queue is empty, try again.
Attempting to get an event from the queue...
(code hangs indefinitely)
Se eu mudar para multiprocessing.Queue
ou usar queue.get(timeout=0.1)
, aparentemente não terei mais esse problema. Mas ambas as abordagens têm um custo/atraso de velocidade substancial, e estou preocupado se alguma delas é totalmente segura, já que até mesmo queue.Queue
é supostamente seguro para threads .
queue.Queue
As filas são seguras para threads. Mas você precisa de reentrada , que é um requisito ainda mais difícil.Quando um manipulador de sinais é executado, ele assume o controle de uma thread, interrompendo qualquer trabalho que esteja sendo executado nela. Todos os tipos de estruturas de dados podem estar em estados inconsistentes nesse ponto. Bloqueios não resolvem o problema, pois um manipulador de sinais pode interromper uma thread enquanto ela mantém um bloqueio e executar nessa thread , com o bloqueio mantido, enquanto quaisquer invariantes que o bloqueio deveria proteger são quebradas.
Neste caso, parece que o seu manipulador de sinais está interrompendo a sua
get
chamada, enquanto mantém o bloqueio da fila. O seu manipulador de sinais tenta chamarput
, que tenta obter o bloqueio da fila novamente. Como o bloqueio da fila é um bloqueio não reentrante, o manipulador de sinais entra em deadlock.Se você precisar de uma fila com a qual seja possível interagir com segurança a partir de manipuladores de sinais, o
queue
módulo fornecequeue.SimpleQueue
isso.queue.SimpleQueue
Ele foi projetado especificamente para queput
possa ser chamado com segurança a partir de um thread que já esteja executando outra chamadaput
ou .get
Observe que
queue.SimpleQueue
é preciso se limitar a uma API mais simples para poder garantir essa segurança. Por exemplo, não há como tornarput
a reentrada segura com uma fila delimitada, portanto,queue.SimpleQueue
não há suporte para limites de tamanho.A solução mais fácil seria usar SimpleQueue, conforme sugerido pela resposta do usuário2357112 - mas preciso manter o suporte para python <3.7, que não o inclui.
Após ler mais sobre reentrada em Python , descobri que sinais só podem ser acionados entre operações atômicas. Assim, troquei de fila para deque :
...tornando-o seguro em relação a threads e sinais para meus propósitos, ao mesmo tempo em que ainda é O(1) nessas operações.
Isso difere do bloqueio do queue.py, que utiliza
threading.Lock
em vez do GIL e, portanto, pode ser interrompido por sinais.Código atualizado para usar deque para manipulação de sinais: