Python의 wait_for에서 동기 도구가 멈추지 않는 이유
요약
본 글은 asyncio 환경에서 동기 도구(synchronous tool)를 사용할 때 발생하는 취소 및 자원 관리 문제를 다룹니다. 특히 `asyncio.to_thread` 사용 시, 태스크가 취소되어도 백그라운드 동기 함수가 계속 실행될 수 있음을 실험을 통해 보여줍니다. 따라서 단순히 대기를 중단하는 것만으로는 부족하며, 호출자와 실행자 측의 타임아웃 및 자원 해제 로직을 명확히 분리하고 관리해야 함을 강조합니다.
핵심 포인트
- `asyncio.to_thread` 사용 시 취소와 실제 동기 함수 종료는 별개로 처리해야 합니다.
- 단순한 `wait_for` 타임아웃은 충분하지 않으며, 자원 해제 로직까지 고려해야 합니다.
- 동기 도구의 경우, 진입점에서 자리를 확보하고 종료될 때까지 유지하는 것이 중요합니다.
- 재시도 로직 구현 시, 상대 측에서 중복을 방지할 수 있는 메커니즘이 필수적입니다.
타임아웃을 짧게 해도, 백그라운드에서 작동하는 동기 도구의 수명은 줄어들지 않는다.
저는 LLM 에이전트의 동기 도구를 asyncio.to_thread에 전달하는 형태를 확인하던 중, 태스크의 cancelled()가 True인데도 동기 함수가 남아있는 곳에서 걸리는 문제에 부딪혔습니다. 같은 업데이트 처리를 바로 재시도하는 것은 위험합니다. 이전 호출이 아직 쓰기에 진행될 가능성이 있기 때문입니다.
이번에는 외부 API를 호출하지 않고, CPython 3.14.8로 재현했습니다. Python 측에서 무엇을 중단시키고 있는지 확인해 보았습니다.
다음 내용을 timeout_thread.py에 저장하고, python3 timeout_thread.py로 실행합니다. 동기 함수의 시작 알림을 기다린 후에 50ms의 타임아웃을 설정했습니다.
import asyncio
import threading
async def main():
...
제가 얻은 출력입니다.
task.cancelled(): True
동기 처리 완료? False
대기 중단 후 완료: True
release.wait()로 동기 함수를 멈추어 두고, 타임아웃 후 release.set()으로 다음 단계로 진행하고 있습니다. 마지막 알림이 도착했기 때문에, 취소된 후에도 동기 함수가 계속 작동했다는 것을 알 수 있었습니다. sleep의 초 단위 값을 조정하는 것보다 순서를 추적하기 쉬웠습니다.
여기서 50ms는 대기에 설정한 값이며, 실제 측정된 응답 시간은 아닙니다.
제가 사용하던 CPython 소스를 읽어보았습니다. asyncio/threads.py의 to_thread는 run_in_executor를 기다리며 기본 스레드 풀에 동기 함수를 전달합니다.
wait_for의 취소는 원래 Future에도 전파됩니다. 다만, concurrent/futures/_base.py의 Future.cancel()은 상태가 RUNNING일 경우 False를 반환하는 구현입니다.
기다리는 쪽의 취소와 실행하는 쪽의 중단은 별개로 다루어야 합니다. 미실행 작업이라면 취소할 수 있는 경우도 있으므로, 재현에서는 시작 알림을 끼워 넣었습니다.
asyncio.Semaphore(1) 내부에서 to_thread를 기다리는 구성도 확인했습니다. 첫 번째 대기를 타임아웃시키고 두 번째 작업을 투입하자, 동기 함수가 2개가 동시에 작동했습니다.
취소로 인해 async with 블록을 빠져나오면서 세마포어의 자리를 반환합니다. 첫 번째 스레드는 남아있으므로, 제한 값 1과 실제 가동 수가 어긋납니다. 대기를 빨리 끊을수록 다음 작업이 흘러들어 옵니다.
대책은 중단시키고 싶은 것에 맞춰 배치해야 합니다.
| 대상 | 구현으로 확인해야 할 부분 |
|---|---|
| 호출하는 쪽의 대기 | wait_for의 기한 |
| 동기 SDK 통신 | SDK나 HTTP 클라이언트의 통신 타임아웃 및 재시도 설정 |
| 가동 중인 동기 처리 수 | 동기 함수의 진입점에서 자리를 확보하고, 함수가 빠져나갈 때까지 유지하기 |
자리 대기가 필요한 작업은 남아있을 수 있으므로, 획득 직후에 마감 기한을 확인하여, 만료되었다면 업데이트를 시작하지 않고 돌아갑니다. 통신 타임아웃으로 돌아와도, 전송된 업데이트가 상대 측에서 완료될 가능성은 남아있습니다.
툴 호출에서는, 대기를 중단시킨 시점과 실제 처리의 종료 시점을 별도로 기록하고 싶습니다. 업데이트를 재시도한다면, 상대 측에서 중복을 방지하는 메커니즘도 필요합니다. 동기 도구가 늘어날수록, 이 시간 차이가 가동 수 예측에 영향을 미칠 것이라고 생각합니다. 우선 cancelled() 옆에 실제 처리 완료 알림을 하나 추가하는 것부터 시작해야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기