8.15. 함정¶
asyncio를 좋게 만드는 바로 그 패턴들 – 선점 없음, 명시적인 await – 은 자신만의 골치 아픈 형태들도 만들어 냅니다. 이 페이지는 알아둘 가치가 있을 만큼 자주 나타나는 것들의 목록입니다.
8.15.1. await를 빠뜨리기¶
async def 함수를 호출하면 코루틴 객체가 반환됩니다. 함수의 본문은 실행되지 않습니다. 실제로 실행하려면 코루틴을 await하거나 태스크로 감싸야 합니다:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
이 버그는 조용합니다 – 코루틴 객체가 생성되고, 버려지고, 결코 실행되지 않습니다. 애플리케이션은 모든 것이 정상 동작한 것처럼 진행됩니다. MicroPython은 때때로 코루틴이 await되지 않았다는 경고를 로그에 남기지만, 그렇지 않을 때도 있습니다. 함수 호출처럼 보이는 모든 호출 지점에서 await가 빠지지 않았는지 점검하십시오.
8.15.2. await 없는 빡빡한 루프¶
루프 안에서 실행되며 결코 await하지 않는 코루틴은 이벤트 루프를 독점합니다. 루프가 종료되거나 제어권을 양보할 때까지 다른 어떤 태스크도 진행하지 못합니다:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
해결책은 루프 안에 yield를 두는 것입니다 – 보통 await asyncio.sleep_ms(0) – 그래야 준비된 다른 태스크들이 실행될 기회를 얻습니다. 연산이 많은 작업도 이 형태 안에 들어가야 합니다: 반복마다 수백 밀리초씩 실행되는 이미지 처리 루프는 반복마다 최소 한 번은 양보하여 프로그램의 나머지 부분이 멈추지 않도록 해야 합니다.
8.15.3. CancelledError를 삼키기¶
취소 페이지에서 이미 이를 자세히 다루었습니다. “내 애플리케이션이 종료되지 않는다”의 가장 흔한 원인이기 때문에 여기서 다시 언급합니다: 코루틴이 정리 목적으로 asyncio.CancelledError를 잡고는 다시 발생시키는 것을 잊는 경우입니다. 태스크는 계속 실행되고, 취소를 요청한 호출자는 그것이 끝나기를 영원히 기다리며 멈춥니다. 정리 후에는 항상 다시 발생시키거나, 명시적인 except 대신 try/finally 블록을 사용하십시오.
8.15.5. 모듈 수준의 await¶
await는 async def 본문 안에서만 유효합니다. 모듈 수준에서 – 어떤 코루틴 외부에서 – 작성하는 것은 구문 오류입니다:
# bug: not inside an async def
result = await fetch()
해결책은 작업을 코루틴 안에 넣고 프로그램의 asyncio.run() 진입점에서 호출하는 것입니다.
8.15.6. 여러 번의 asyncio.run 호출¶
MicroPython에는 이벤트 루프가 하나뿐입니다. asyncio.run()을 연달아 두 번 호출하는 것 – 한 번은 설정용, 한 번은 주 작업용 – 도 여전히 같은 루프를 사용합니다. 실행 중인 코루틴 내부에서 호출하는 것은 오류입니다: 루프가 이미 실행 중이기 때문입니다. 두 경우 모두 스크립트가 자연스럽게 커지면서 작성자가 새 작업을 기존 main에 합치는 대신 run() 호출을 더 추가하여 확장하려 할 때 가장 자주 발생합니다.
8.15.7. 인터럽트에서 Event 사용하기¶
asyncio.Event.set()은 이벤트 루프 내부에서 호출할 때만 안전합니다. GPIO 인터럽트 핸들러에서 호출하는 것은 손상 위험이 있습니다. 인터럽트에서 태스크를 깨우려면 대신 ThreadSafeFlag를 사용하십시오 – 이에 관한 페이지에서 그 형태를 다룹니다.
8.15.8. 긴 동기 호출¶
코루틴은 asyncio 자체의 대기 프리미티브를 await할 수 있지만, 그 외에 호출하는 모든 것은 동기적으로 실행되어 반환될 때까지 루프를 차단합니다. 200ms 동안 차단하는 time.sleep(), 플러시에 80ms가 걸리는 SD 카드 쓰기, 큰 JPEG 압축, csi.CSI.snapshot() 호출 – 이들 각각은 그 전체 기간 동안 이벤트 루프를 붙잡습니다. 해결책은 호출에 따라 다릅니다:
time.sleep의 경우:await asyncio.sleep또는await asyncio.sleep_ms로 교체하십시오.csi.CSI.snapshot의 경우: 캡처 페이지에서 만드는 비동기 스냅샷 래퍼를 사용하십시오.긴 연산(이미지 처리, JPEG 인코딩)의 경우: 그 비용을 감수하거나, 작업을 반복 사이에서
await하는 청크로 나누십시오.
asyncio는 동기 호출을 논블로킹으로 만들 수 없습니다. 다른 무언가가 await되는 동안 다른 코루틴이 실행되도록 할 수 있을 뿐입니다.