8.8. 락(Lock)¶
asyncio.Lock 은 코루틴 간의 상호 배제(mutual exclusion) 를 제공합니다. 즉, 한 번에 하나의 코루틴만 락을 보유하고 나머지는 보유자가 락을 해제할 때까지 대기한다는 보장을 제공합니다.
8.8.1. 락이 필요한 경우¶
협력적 동시성 페이지에서 두 코루틴은 await 가 없는 코드의 중간에서 서로 끼어들 수 없다고 언급했습니다. 반대 도 성립합니다. 코루틴이 await 하는 순간, 루프는 다른 코루틴을 자유롭게 실행할 수 있습니다. 두 코루틴이 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)
이제 두 코루틴은 모두 read_register 를 동시에 호출할 수 있습니다. 락은 한 번에 그 중 하나만 버스를 보유하도록 보장하며, 다른 하나는 락이 해제될 때까지 기다린 뒤에 시작합니다.
임계 영역 안에 await 가 없을 때는 락이 필요하지 않습니다 . 협력적 스케줄링 보장이 이미 그 경우를 다루기 때문입니다. 락은 임계 영역이 중간에 루프로 제어를 양보할 때만 필요합니다.
8.8.2. 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.
락의 수명이 코드 블록과 일치하지 않는 드문 경우를 위해, 메서드를 직접 사용할 수도 있습니다:
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()