threading.Timer는 one-shot non-daemon 스레드라 반복과 종료를 직접 다뤄야 한다
·수정 2026.06.02·수정 1회
요약
threading.Timer는 기본 non-daemon이라 메인이 끝나도 살아있으면 프로세스 종료를 막는다 → 거의 항상daemon=True.- 한 번만 발화하는 one-shot이라 주기 실행은 콜백에서 스스로 다시 거는 self-rescheduling으로 만든다.
- self-rescheduling은 tick 중첩이 없어(이전 작업 후 재무장) setInterval보다 안전하고, 종료는 플래그 + cancel()로 제어한다.
본문
기본 동작과 함정
Timer는 Thread의 서브클래스이고, Thread는 기본적으로 non-daemon이다.
from threading import Timer
t = Timer(60, fire)
t.start()
# 메인 코드 끝나도 60초 동안 프로세스 종료 안 됨
Non-daemon 스레드는 다 끝날 때까지 프로세스 종료를 막는다. 이걸 모르면 "왜 프로세스가 안 죽지?" 디버깅에 시간을 쓰게 된다.
Daemon=True가 맞는 경우 (대다수)
Timer가 보통 쓰이는 용도:
- Debounce, throttle 패턴
- UI 알림/팝업 자동 닫기
- 타임아웃 경고
- 헬스체크/하트비트
- 빈 채널 타임아웃, grace period
- 취소될 수 있는 예약 작업
이들은 모두 **"지연된 부수 작업"**이라 메인이 끝나면 같이 사라져도 무방하다.
t = Timer(30, fire)
t.daemon = True
t.start()
Daemon=False를 고려할 경우 (드뭄)
타이머가 반드시 끝까지 실행되어야 하는 중요 작업:
- 버퍼 flush
- DB 커밋
- 파일 저장
하지만 이런 경우엔 사실 Timer가 적합한 도구가 아니다. 더 적절한 패턴:
# Timer로 지연 cleanup — 위험: 메인 종료되면 잘릴 수 있음
Timer(5, save_to_disk).start()
# 더 좋은 방법
import atexit
atexit.register(save_to_disk) # 종료 시 항상 실행 보장
또는 명시적 shutdown 핸들러:
def shutdown():
timer.cancel()
save_to_disk()
실용 가이드
| 상황 | 설정 |
|---|---|
| 일반적인 Timer 사용 | daemon=True |
| 메인이 끝나도 꼭 실행되어야 함 | Timer 말고 atexit 또는 명시적 shutdown |
| 라이브러리 코드 | daemon=True (호출자 종료 막으면 민폐) |
한 줄 정리
Timer = daemon=True가 거의 정답. daemon=False가 필요한 상황이라면 애초에 Timer가 아닌 다른 도구가 더 맞다는 신호다.
Thread 일반은 케이스마다 다르지만, Timer만큼은 거의 항상 daemon으로 간주해도 무방하다.
또 다른 함정: Timer는 one-shot이다
threading.Timer는 한 번만 발화한다. setInterval처럼 자동 반복하지 않는다.
t = Timer(1, fire)
t.start() # 1초 뒤 fire() 딱 한 번. 끝.
주기 실행이 필요하면 콜백이 끝날 때 스스로 다음 타이머를 다시 거는(self-rescheduling) 구조로 만든다.
def _tick(self):
try:
self._evaluate() # 실제 일
finally:
with self._lock:
if not self._stopped: # stop() 플래그로 루프 종료
self._arm() # 다음 1초 타이머를 다시 건다
def _arm(self):
self._timer = Timer(1, self._tick)
self._timer.daemon = True
self._timer.start()
setInterval과의 차이 (중요):
- 고정 간격이 아니라
_evaluate()실행 시간 + 1초가 실제 간격이다. 이전 tick이 끝난 뒤에야 다음 타이머를 걸기 때문. - 그래서 tick이 겹쳐 실행될 일이 없다. 무거운 작업을 주기 실행할 때 setInterval보다 안전하다 (중첩 방지).
- 종료는
_stopped플래그 + 마지막 타이머cancel()로. 재무장을 안 하면 루프가 자연히 끝난다.
cleanup이 무거워서 매 tick마다 lock을 오래 잡으면 안 되는 경우, 판정만 lock 안에서 하고 부수 작업(native 호출 등)은 lock 밖에서 실행하는 패턴과 잘 어울린다.