8.8. Blocaje

asyncio.Lock oferă excludere mutuală între corutine – o garanție că o singură corutină deține blocajul la un moment dat, iar celelalte așteaptă până când deținătorul îl eliberează.

8.8.1. Când este nevoie de un blocaj

Pagina concurență cooperativă a menționat că două corutine nu se pot întrețese la jumătatea unui cod care nu conține niciun await. Și opusul este adevărat: imediat ce o corutină execută await, bucla este liberă să ruleze o altă corutină. Dacă două corutine accesează aceeași resursă de-a lungul mai multor await-uri – un bus UART, I2C sau SPI – operațiile lor se pot întrețese în moduri care corup resursa.

Un blocaj în jurul secțiunii critice închide această breșă:

import asyncio

bus_lock = asyncio.Lock()

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

Acum, două corutine pot apela ambele read_register concurent; blocajul se asigură că doar una dintre ele deține bus-ul la un moment dat, iar cealaltă așteaptă eliberarea blocajului înainte de a începe.

Blocajele nu sunt necesare atunci când secțiunea critică nu conține niciun await în interior – garanția planificării cooperative acoperă deja acest caz. Sunt necesare doar atunci când secțiunea critică cedează controlul buclei la jumătatea execuției.

8.8.2. Idiomul 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.

Pentru cazul rar în care durata de viață a blocajului nu coincide cu un bloc de cod, metodele sunt disponibile și direct:

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

Construcția try/finally este necesară pentru a face acest lucru echivalent cu versiunea async with. Forma async with există pentru că aceasta este forma corectă, iar limbajul o face concisă.

8.8.3. Referință a metodelor

  • acquire() – o corutină. Blochează până când blocajul este deblocat, apoi îl preia.

  • release() – eliberează blocajul. Dacă există corutine în coadă care așteaptă pe acquire(), următoarea din coadă este planificată să ruleze, iar blocajul rămâne blocat; altfel, blocajul devine deblocat.

  • locked() – returnează True dacă blocajul este deținut în prezent, False în caz contrar. Returnează imediat; nu blochează.

Cei care așteaptă sunt deserviți în ordine FIFO. Nu există prioritate, nici reintrare (aceeași sarcină nu poate prelua un blocaj pe care îl deține deja) și niciun timeout la preluare. Pentru a impune un termen-limită unei preluări de blocaj, încadrați preluarea în 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()