8.8. Locks¶
asyncio.Lock stellt einen gegenseitigen Ausschluss zwischen Coroutinen bereit – eine Garantie, dass immer nur eine Coroutine gleichzeitig den Lock hält, während die anderen warten, bis der Halter ihn freigibt.
8.8.1. Wann ein Lock benötigt wird¶
Die Seite Kooperative Nebenläufigkeit hat darauf hingewiesen, dass zwei Coroutinen sich nicht mitten in Code verzahnen können, der kein await enthält. Das Gegenteil gilt ebenfalls: Sobald eine Coroutine awaitet, kann der Loop eine andere Coroutine ausführen. Wenn zwei Coroutinen über Awaits hinweg dieselbe Ressource berühren – einen UART-, I2C- oder SPI-Bus – können sich ihre Operationen so verzahnen, dass die Ressource beschädigt wird.
Ein Lock um den kritischen Abschnitt schließt diese Lücke:
import asyncio
bus_lock = asyncio.Lock()
async def read_register(bus, addr):
async with bus_lock:
bus.write(addr)
return await bus.read(2)
Zwei Coroutinen können nun beide gleichzeitig read_register aufrufen; der Lock stellt sicher, dass immer nur eine von ihnen den Bus hält, und die andere wartet, bis der Lock freigegeben wird, bevor sie startet.
Locks werden nicht benötigt, wenn der kritische Abschnitt kein await enthält – die Garantie der kooperativen Planung deckt diesen Fall bereits ab. Sie werden nur benötigt, wenn der kritische Abschnitt mittendrin an den Loop abgibt.
8.8.2. Das async with-Idiom¶
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.
Für den seltenen Fall, dass die Lebensdauer des Locks nicht mit einem Codeblock übereinstimmt, sind die Methoden auch direkt verfügbar:
await bus_lock.acquire()
try:
bus.write(addr)
result = await bus.read(2)
finally:
bus_lock.release()
Das try/finally ist erforderlich, um dies äquivalent zur async with-Version zu machen. Die async with-Form existiert, weil dies die richtige Struktur ist und die Sprache sie prägnant macht.
8.8.3. Methodenreferenz¶
acquire()– eine Coroutine. Blockiert, bis der Lock entsperrt ist, und übernimmt ihn dann.release()– gibt den Lock frei. Wenn Coroutinen in der Warteschlange aufacquire()warten, wird die nächste in der Warteschlange zur Ausführung eingeplant und der Lock bleibt gesperrt; andernfalls wird der Lock entsperrt.locked()– gibtTruezurück, wenn der Lock derzeit gehalten wird, andernfallsFalse. Kehrt sofort zurück; blockiert nicht.
Wartende werden in FIFO-Reihenfolge bedient. Es gibt keine Priorität, keine Reentrancy (derselbe Task kann keinen Lock erwerben, den er bereits hält) und keinen Timeout beim Erwerben. Um einer Lock-Erwerbung eine Frist zu setzen, umschließe das Acquire mit 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()