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#
L’esempio sopra mostra il modo consigliato di usare un lock: all’interno di un blocco async with. All’ingresso il blocco esegue await su acquire(); all’uscita (sia che il blocco sia terminato normalmente, abbia sollevato un’eccezione o sia stato annullato) il lock viene rilasciato automaticamente. Non esiste alcuna via d’uscita dal blocco che lasci il lock detenuto.
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 suacquire(), la successiva nella coda viene schedulata per l’esecuzione e il lock resta bloccato; altrimenti il lock diventa sbloccato.locked()– restituisceTruese il lock e attualmente detenuto,Falsealtrimenti. 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()