8.8. Locks

asyncio.Lock biedt wederzijdse uitsluiting tussen coroutines – de garantie dat slechts één coroutine tegelijk de lock vasthoudt, terwijl de andere wachten tot de houder hem vrijgeeft.

8.8.1. Wanneer een lock nodig is

Op de pagina coöperatieve concurrency werd opgemerkt dat twee coroutines elkaar niet halverwege code kunnen onderbreken als die code geen await bevat. Het omgekeerde geldt ook: zodra een coroutine awaitt, is de loop vrij om een andere coroutine uit te voeren. Als twee coroutines dezelfde bron aanraken over awaits heen – een UART, I2C- of SPI-bus – kunnen hun bewerkingen op manieren door elkaar lopen die de bron beschadigen.

Een lock rond de kritieke sectie dicht het gat:

import asyncio

bus_lock = asyncio.Lock()

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

Twee coroutines kunnen nu beide tegelijk read_register aanroepen; de lock zorgt ervoor dat slechts één van hen de bus tegelijk vasthoudt, en de andere wacht tot de lock wordt vrijgegeven voordat hij begint.

Locks zijn niet nodig wanneer de kritieke sectie geen await bevat – de garantie van coöperatieve planning dekt dat geval al. Ze zijn alleen nodig wanneer de kritieke sectie halverwege de controle teruggeeft aan de loop.

8.8.2. Het async with-idioom

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.

Voor het zeldzame geval waarin de levensduur van de lock niet samenvalt met een codeblok, zijn de methoden ook rechtstreeks beschikbaar:

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

De try/finally is vereist om dit gelijkwaardig te maken aan de async with-versie. De async with-vorm bestaat omdat dit de juiste vorm is en de taal hem beknopt maakt.

8.8.3. Methodereferentie

  • acquire() – een coroutine. Blokkeert tot de lock ontgrendeld is en neemt hem dan over.

  • release() – geeft de lock vrij. Als er coroutines in de wachtrij staan die op acquire() wachten, wordt de volgende in de wachtrij ingepland om uit te voeren en blijft de lock vergrendeld; anders wordt de lock ontgrendeld.

  • locked() – geeft True terug als de lock momenteel wordt vastgehouden, anders False. Keert onmiddellijk terug; blokkeert niet.

Wachtenden worden in FIFO-volgorde bediend. Er is geen prioriteit, geen heringang (dezelfde taak kan een lock die hij al vasthoudt niet opnieuw verkrijgen) en geen time-out bij het verkrijgen. Om een deadline op het verkrijgen van een lock te zetten, omhul je het verkrijgen met 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()