8.8. Locks#

asyncio.Lock fornece exclusão mútua entre corrotinas – a garantia de que apenas uma corrotina por vez detém o lock, enquanto as outras aguardam até que o detentor o libere.

8.8.1. Quando um lock é necessário#

A página concorrência cooperativa observou que duas corrotinas não podem se intercalar no meio de um trecho de código que não contém nenhum await. O oposto também é verdadeiro: assim que uma corrotina faz await, o loop fica livre para executar outra corrotina. Se duas corrotinas acessam o mesmo recurso entre awaits – um barramento UART, I2C ou SPI – suas operações podem se intercalar de maneiras que corrompem o recurso.

Um lock em torno da seção crítica fecha essa lacuna:

import asyncio

bus_lock = asyncio.Lock()

async def read_register(bus, addr):
    async with bus_lock:
        bus.write(addr)
        return await bus.read(2)

Agora ambas as corrotinas podem chamar read_register simultaneamente; o lock garante que apenas uma delas detenha o barramento por vez, e a outra aguarda a liberação do lock antes de começar.

Locks não são necessários quando a seção crítica não contém nenhum await – a garantia do escalonamento cooperativo já cobre esse caso. Eles só são necessários quando a seção crítica cede o controle ao loop no meio do caminho.

8.8.2. O idioma async with#

O exemplo acima mostra a forma recomendada de usar um lock: dentro de um bloco async with. Na entrada, o bloco faz await em acquire(); na saída (seja porque o bloco retornou normalmente, lançou uma exceção ou foi cancelado) o lock é liberado automaticamente. Não há nenhum caminho de saída do bloco que deixe o lock retido.

Para o caso raro em que o tempo de vida do lock não coincide com um bloco de código, os métodos também estão disponíveis diretamente:

await bus_lock.acquire()
try:
    bus.write(addr)
    result = await bus.read(2)
finally:
    bus_lock.release()

O try/finally é necessário para tornar isso equivalente à versão com async with. A forma async with existe porque este é o formato correto e a linguagem o torna conciso.

8.8.3. Referência de métodos#

  • acquire() – uma corrotina. Bloqueia até que o lock seja destravado e então o adquire.

  • release() – libera o lock. Se houver corrotinas enfileiradas aguardando em acquire(), a próxima da fila é escalonada para execução e o lock permanece travado; caso contrário, o lock fica destravado.

  • locked() – retorna True se o lock estiver atualmente retido, False caso contrário. Retorna imediatamente; não bloqueia.

As tarefas em espera são atendidas em ordem FIFO. Não há prioridade, nem reentrância (a mesma tarefa não pode adquirir um lock que já detém) e nenhum timeout na aquisição. Para impor um prazo a uma aquisição de lock, envolva o acquire em asyncio.wait_for()

try:
    await asyncio.wait_for(bus_lock.acquire(), timeout=1)
except asyncio.TimeoutError:
    # lock busy for >1 s -- bail out
    return None
try:
    ...
finally:
    bus_lock.release()