터미널에 타임아웃이 반환되었습니다. 오늘은 동기형 LLM 클라이언트를 별도의 스레드에서 호출하는 구성으로, 백그라운드에 남아있는 처리를 계산해 보았습니다.
저는 대기 시간과 실제 처리의 동시 실행 수를 각각 제한합니다. asyncio.Semaphore(1)을 사용했음에도 불구하고, 타임아웃이 발생하자 실제 처리가 3개까지 증가했습니다.
CPython 3.14.8에서 실행했습니다. 표준 라이브러리만을 사용하고, 동기 클라이언트의 대용으로 blocking()을 사용합니다. API 연결은 없습니다.
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
...
출력:
workers=3, active=3
workers=1, active=1
active는 실행 중인 개수입니다. release가 호출될 때까지 종료되지 않기 때문에, 처리 시간의 우연에 의존하지 않고 계산할 수 있습니다. 시작 알림도 기다렸습니다. 풀(pool)이 1개일 경우 첫 번째 함수가 자리를 차지하기 때문에, 두 번째 호출부터는 시작 알림을 기다리지 않습니다.
wait_for()의 기한이 오면 Task가 취소됩니다. async with gate를 빠져나가면서 Semaphore의 슬롯이 반환되고 다음 호출이 들어옵니다. 실행 중인 스레드는 release.wait()에 남아있습니다.
슬롯은 비었습니다. 함수는 끝나지 않았습니다.
to_thread 구현은 run_in_executor()에 함수를 전달합니다. Future.cancel은 RUNNING 상태의 Future를 취소할 수 없습니다. 실행 중인 함수를 강제 종료하는 메커니즘이 아닙니다.
20ms는 대기 기한입니다.
두 번째로는 max_workers=1로 변경했습니다. Semaphore가 반환되더라도, 다음 함수는 줄지어 기다립니다. 미착수된 호출은 취소할 수 있습니다.
애플리케이션에서는 다른 처리도 기본 풀(default pool)을 사용하므로, LLM 전용 풀을 run_in_executor()에 전달하고 종료 처리도 준비합니다. asyncio.run() 역시 기본 풀의 완료를 기다리기 때문에, 이 코드에서 finally 블록을 제거하면 종료 대기 상태로 멈춥니다.
스레드 수의 상한만으로는 대기열을 제한할 수 없습니다. 접수 건수에도 상한을 두고, 동기 클라이언트에는 통신 타임아웃을 설정해야 합니다. 서버 측의 생성이나 과금까지 멈추는지는 API 사양을 확인해야 합니다.
타임아웃 건수만으로는 남아있는 처리를 계산하지 못합니다. 클라이언트 호출의 시작과 종료를, 대기 측의 종료와 분리하여 기록해야 합니다. 동기 SDK가 늘어날수록 이 차이는 재시도 시 부하에 영향을 미친다고 보고 있습니다. 위의 코드라면 API를 호출하지 않고 잔존 개수를 확인할 수 있습니다.