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

The example above shows the recommended way to use a lock: inside an async with block. On entry the block awaits acquire(); on exit (whether the block returned normally, raised an exception, or was cancelled) the lock is released automatically. There is no path out of the block that leaves the lock held.

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()