8.8. Fechos

asyncio.Lock fornece exclusão mútua entre coroutines – uma garantia de que apenas uma coroutine de cada vez detém o fecho, e as outras aguardam até que o detentor o liberte.

8.8.1. Quando um fecho é necessário

A página de concorrência cooperativa notou que duas coroutines não podem intercalar-se a meio de código que não tem nenhum await nele. O oposto também é verdade: logo que uma coroutine faz await, o ciclo fica livre para correr outra coroutine. Se duas coroutines tocam no mesmo recurso entre awaits – um bus UART, I2C ou SPI – as suas operações podem intercalar-se de formas que corrompem o recurso.

Um fecho em torno da secção crítica fecha a 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)

Duas coroutines podem agora ambas chamar read_register concorrentemente; o fecho garante que apenas uma delas detém o bus de cada vez, e a outra aguarda que o fecho seja libertado antes de começar.

Os fechos não são necessários quando a secção crítica não tem nenhum await dentro dela – a garantia de agendamento cooperativo já cobre esse caso. Só são necessários quando a secção crítica cede ao ciclo a meio.

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 fecho não se alinha 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 isto equivalente à versão async with. A forma async with existe porque esta é a forma correta e a linguagem torna-a concisa.

8.8.3. Referência de métodos

  • acquire() – uma coroutine. Bloqueia até o fecho estar desbloqueado, depois toma-o.

  • release() – liberta o fecho. Se houver coroutines em fila à espera em acquire(), a próxima na fila é agendada para correr e o fecho permanece bloqueado; caso contrário o fecho fica desbloqueado.

  • locked() – devolve True se o fecho está atualmente retido, False caso contrário. Retorna imediatamente; não bloqueia.

Os aguardadores são servidos por ordem FIFO. Não há prioridade, nem reentrância (a mesma tarefa não pode adquirir um fecho que já detém), nem timeout na aquisição. Para colocar um prazo numa aquisição de fecho, encapsule a aquisição 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()