thread_obj.start()가 즉시 반환된다는 점이 핵심 문제입니다. 시작된 자식 스레드는 자신만의 독립적인 컨텍스트와 스택에서 실행되며, 그곳에서 발생하는 모든 예외 역시 자식 스레드의 컨텍스트에 속하게 됩니다.
따라서 부모 스레드(호출자)가 이 예외를 알 수 있게 하려면, 메시지 전달 방식으로 예외 정보를 부모 스레드에 넘겨주어야 합니다. 가장 일반적인 방법은 queue.Queue를 사용하여 예외 정보(sys.exc_info())를 전달하는 것입니다.
예외를 큐로 전달하는 구현 예제
아래와 같이 코드를 다시 작성할 수 있습니다:
import sys
import threading
import queue
class ExcThread(threading.Thread):
def __init__(self, foo):
threading.Thread.__init__(self)
self.foo = foo
def run(self):
try:
raise Exception('An error occurred here.')
except Exception:
# 예외 정보를 큐에 담아 부모 스레드에 전달
self.foo.put(sys.exc_info())
def main():
foo = queue.Queue()
thread_obj = ExcThread(foo)
thread_obj.start()
while True:
try:
exc = foo.get(block=False)
except queue.Empty:
pass
else:
exc_type, exc_obj, exc_trace = exc
print(exc_type, exc_obj)
print(exc_trace)
thread_obj.join(0.1)
if thread_obj.is_alive():
continue
코드 동작 원리
1. 예외 캡처: 자식 스레드의 run() 메서드 내부에서 try-except문 블록으로 예외를 잡은 후, sys.exc_info()를 사용해 예외 유형(type), 예외 객체(object), 트레이스백(traceback)을 포함한 전체 정보를 큐에 저장합니다.
2. 예외 수신: 메인 스레드는 while 루프를 돌며 큐를 확인합니다. block=False 옵션으로 논블로킹 방식으로 데이터를 가져오고, 큐가 비어 있으면 queue.Empty 예외를 무시합니다.
3. 종료 대기: thread_obj.join(0.1)로 최대 0.1초간 자식 스레드의 종료를 기다리고, 아직 살아 있다면(is_alive()) 루프를 계속 반복합니다.
참고 사항
위 코드는 파이썬 3 기준으로 작성되었습니다. 파이썬 2에서는 queue 모듈 대신 Queue 모듈을 사용하고, is_alive() 대신 isAlive()를 사용하십시오.
또한 실무에서는 폴링(polling) 방식보다 queue.get(block=True) 또는 타임아웃을 활용하는 편이 CPU 자원을 절약할 수 있으며, concurrent.futures.ThreadPoolExecutor를 사용하면 future.result() 호출 시 자동으로 자식 스레드의 예외가 재발생되므로 더 간결하게 처리할 수 있습니다.