Tenho um serviço Python que usa threads virtuais do Python ( threading.Thread
) para lidar com solicitações. Há uma funcionalidade singleton compartilhada que todas as threads tentam acessar, protegida por threading.Lock
.
g_lock = threading.Lock()
def my_threaded_functionality():
try:
g_lock.acquire()
# ... Do something with a shared resource ...
finally:
g_lock.release()
Nos documentos de threading.Lock.acquire
, não há menção à justiça , enquanto que, no de asyncio asyncio.Lock.acquire
, eles mencionam que o bloqueio é justo.
Como quero evitar a inanição de threads e preservar a ordem das tarefas como elas chegaram, eu optaria pelo asyncio
Lock de 's se eles não mencionassem que os bloqueios não são seguros para threads. A questão é se isso também deveria ser um problema com as "threads" virtuais do Python.
threading.Lock
não é oficialmente justo, no entanto, pode realmente se comportar dessa maneira.Em CPython<3.13 ele é implementado via semáforo POSIX [1] [2] , o que só é justo se uma política de escalonamento for especificada que forneça tal garantia .
Em CPython>=3.13, isso é implementado por meio de um estacionamento (uma implementação de fila de espera) e operações CAS (comparação e troca) atômicas [3] [4] (consulte cpython/python#108724 ). Um recurso interessante é que
threading.Lock
agora é efetivamente justo para aqueles que esperam mais de um milissegundo [5] : se mais de um milissegundo se passar entre o enfileiramento e a liberação, a thread em espera imediatamente se torna a proprietária do bloqueio e, caso contrário, compete com outras threads pela propriedade do bloqueio.Se você não quiser depender das especificações de implementação da biblioteca padrão e realmente precisar se preocupar com a escassez de recursos, pode usar
aiologic.Lock
(sou o criador do aiologic ), que é garantido como justo. Ele é compatível com threads (thread-safe) e assíncrono, e pode ser usado simultaneamente em threads e tarefas assíncronas. Devido a essa natureza dupla, ele tem uma interface ligeiramente diferente eaiologic.Lock
segue a semântica de bibliotecas como Trio e AnyIO: suasrelease()
chamadas verificam se a thread/tarefa atual é a proprietária do bloqueio e, caso contrário, geram um erroRuntimeError
.O aiologic também fornece outras primitivas com as quais você está familiarizado, como semáforos e filas. Todas elas são justas e se preocupam não apenas com a imparcialidade, mas também em evitar as formas mais exóticas de escassez de recursos associadas a especificações de agendamento (que fazem com que a implementação seja livre de bloqueios). No entanto, esta é uma biblioteca Python pura, então elas tendem a ser mais lentas do que algumas de suas análogas do módulo de threading implementadas em C (
threading.Lock
ethreading.RLock
).As threads do CPython são threads nativas, não threads virtuais. O conceito de threads virtuais não existe no CPython.
O bloqueio do asyncio não é seguro para threads, você não pode usá-lo para sincronização multithread, ele só
threading.Lock
é seguro para acesso multithread.você pode serializar o acesso a esse recurso com um threadpool de 1 thread, ele tem uma fila interna e garante justiça (primeiro a entrar, primeiro a sair), não use bloqueios. como bônus, você pode usar loop.run_in_executor para aguardá-lo em seus eventloops.
concurrent.futures.ThreadPoolExecutor
gera threads preguiçosamente, então não há problema em tê-lo no escopo global, ele não cria uma thread se não for usado, mas prefiro encapsular tudo em uma classe.Nota: enviar trabalho para outros threads e vice-versa adiciona aproximadamente 10 a 50 microssegundos de latência, use-o apenas se precisar garantir a ordem, caso contrário, use apenas um
threading.Lock
há também uma versão assíncrona para bloquear
threading.Lock
um eventloop (que também tem esses 10-50 microssegundos extras de sobrecarga..., eu provavelmente usaria o thread_pool de 1 trabalhador se você estiver em código assíncrono, que também é multithread)