8.8. ロック#

asyncio.Lock はコルーチン間の 相互排他 を提供します。つまり、一度にロックを保持できるコルーチンは 1 つだけであり、他のコルーチンは保持者がロックを解放するまで待機することが保証されます。

8.8.1. ロックが必要になるとき#

協調的並行性 のページでは、await を含まないコードの途中で 2 つのコルーチンが交互に実行されることはないと説明しました。その も真です。コルーチンが await した瞬間に、ループは別のコルーチンを自由に実行できるようになります。2 つのコルーチンが await をまたいで同じリソース(UART、I2C、SPI バスなど)に触れる場合、それらの操作が交錯してリソースを破壊してしまう可能性があります。

クリティカルセクションをロックで囲むと、この隙間をふさげます:

import asyncio

bus_lock = asyncio.Lock()

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

これで 2 つのコルーチンがどちらも同時に read_register を呼び出せますが、ロックによって一度にバスを保持できるのは 1 つだけになり、もう一方はロックが解放されるまで開始を待ちます。

クリティカルセクションの内部に await がない場合、ロックは 不要 です。協調的スケジューリングの保証がすでにそのケースをカバーしているためです。ロックが必要なのは、クリティカルセクションが途中でループに制御を譲る場合だけです。

8.8.2. async with イディオム#

上の例は、ロックを使う推奨される方法、すなわち async with ブロックの内側で使う方法を示しています。ブロックに入るときに acquire()await し、ブロックを抜けるとき(正常に返った場合でも、例外を送出した場合でも、キャンセルされた場合でも)にロックは自動的に解放されます。ロックを保持したままブロックを抜ける経路は存在しません。

ロックの存続期間がコードブロックと一致しないまれなケースのために、メソッドを直接使うこともできます:

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

try/finally は、これを async with 版と等価にするために必要です。async with 形式が存在するのは、これ が正しい形であり、言語がそれを簡潔に書けるようにしているからです。

8.8.3. メソッドリファレンス#

  • acquire() -- コルーチンです。ロックがアンロックされるまでブロックし、その後ロックを取得します。

  • release() -- ロックを解放します。acquire() で待機中のコルーチンがキューに並んでいる場合、キュー内の次のコルーチンが実行されるようにスケジュールされ、ロックはロックされたままになります。そうでなければロックはアンロック状態になります。

  • locked() -- ロックが現在保持されていれば True を、そうでなければ False を返します。即座に返り、ブロックしません。

待機中のコルーチンは FIFO 順に処理されます。優先度はなく、再入もできず(同じタスクがすでに保持しているロックを取得することはできません)、acquire にタイムアウトもありません。ロックの取得に期限を設けるには、acquire を 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()