8.8. Lock

asyncio.Lock fornisce la mutua esclusione tra coroutine: la garanzia che soltanto una coroutine alla volta detenga il lock, mentre le altre attendono finche chi lo detiene non lo rilascia.

8.8.1. Quando serve un lock

La pagina sulla concorrenza cooperativa ha osservato che due coroutine non possono intercalarsi a meta di codice che non contiene alcun await. Vale anche l”opposto: non appena una coroutine esegue un await, il loop e libero di eseguire un’altra coroutine. Se due coroutine toccano la stessa risorsa attraverso gli await – un bus UART, I2C o SPI – le loro operazioni possono intercalarsi in modi che corrompono la risorsa.

Un lock attorno alla sezione critica chiude questa 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)

Ora due coroutine possono entrambe chiamare read_register in modo concorrente; il lock garantisce che solo una di esse detenga il bus alla volta, mentre l’altra attende che il lock venga rilasciato prima di iniziare.

I lock non sono necessari quando la sezione critica non contiene alcun await al suo interno: la garanzia dello scheduling cooperativo copre gia quel caso. Servono soltanto quando la sezione critica cede il controllo al loop a meta del proprio svolgimento.

8.8.2. L’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.

Per il raro caso in cui la durata del lock non coincida con un blocco di codice, i metodi sono disponibili anche direttamente:

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

Il try/finally e necessario per rendere questo equivalente alla versione con async with. La forma async with esiste perche questa e la struttura corretta e il linguaggio la rende concisa.

8.8.3. Riferimento dei metodi

  • acquire() – una coroutine. Si blocca finche il lock non e sbloccato, poi lo acquisisce.

  • release() – rilascia il lock. Se delle coroutine sono in coda in attesa su acquire(), la successiva nella coda viene schedulata per l’esecuzione e il lock resta bloccato; altrimenti il lock diventa sbloccato.

  • locked() – restituisce True se il lock e attualmente detenuto, False altrimenti. Restituisce immediatamente; non si blocca.

Chi e in attesa viene servito in ordine FIFO. Non c’e priorita, ne rientranza (lo stesso task non puo acquisire un lock che gia detiene), ne timeout sull’acquisizione. Per imporre una scadenza sull’acquisizione di un lock, racchiudi l’acquire in 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()