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#
Das obige Beispiel zeigt die empfohlene Art, einen Lock zu verwenden: innerhalb eines async with-Blocks. Beim Eintritt awaitet der Block acquire(); beim Austritt (ob der Block normal zurückkehrte, eine Ausnahme auslöste oder abgebrochen wurde) wird der Lock automatisch freigegeben. Es gibt keinen Weg aus dem Block heraus, der den Lock gehalten zurücklässt.
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()