8.8. Bloqueos (Locks)

asyncio.Lock proporciona exclusión mutua entre corrutinas: la garantía de que solo una corrutina mantiene el bloqueo a la vez, y las demás esperan hasta que quien lo posee lo libera.

8.8.1. Cuándo se necesita un bloqueo

La página sobre concurrencia cooperativa señalaba que dos corrutinas no pueden intercalarse a mitad de un fragmento de código que no contiene ningún await. Lo contrario también es cierto: en cuanto una corrutina hace await, el bucle queda libre para ejecutar otra corrutina. Si dos corrutinas acceden al mismo recurso a través de awaits – un bus UART, I2C o SPI – sus operaciones pueden intercalarse de formas que corrompen el recurso.

Un bloqueo alrededor de la sección crítica cierra esa brecha:

import asyncio

bus_lock = asyncio.Lock()

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

Ahora ambas corrutinas pueden llamar a read_register de forma concurrente; el bloqueo se asegura de que solo una de ellas tenga el bus a la vez, y la otra espera a que el bloqueo se libere antes de empezar.

Los bloqueos no son necesarios cuando la sección crítica no contiene ningún await en su interior – la garantía de la planificación cooperativa ya cubre ese caso. Solo hacen falta cuando la sección crítica cede el control al bucle a mitad de camino.

8.8.2. El modismo 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 el caso poco frecuente en el que la vida útil del bloqueo no coincide con un bloque de código, los métodos también están disponibles directamente:

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

El try/finally es necesario para que esto sea equivalente a la versión con async with. La forma async with existe porque esta es la estructura correcta y el lenguaje la hace concisa.

8.8.3. Referencia de métodos

  • acquire() – una corrutina. Se bloquea hasta que el bloqueo esté desbloqueado y entonces lo toma.

  • release() – libera el bloqueo. Si hay corrutinas en cola esperando en acquire(), la siguiente de la cola se planifica para ejecutarse y el bloqueo permanece bloqueado; en caso contrario, el bloqueo pasa a estar desbloqueado.

  • locked() – devuelve True si el bloqueo está actualmente retenido, y False en caso contrario. Devuelve inmediatamente; no se bloquea.

Los que esperan se atienden en orden FIFO. No hay prioridad, ni reentrada (la misma tarea no puede adquirir un bloqueo que ya posee), ni tiempo de espera en la adquisición. Para poner un límite de tiempo a la adquisición de un bloqueo, envuelve el acquire en 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()