threading — 스레드 기반 병렬성
threading — 스레드 기반 병렬성
이 모듈은 저수준 _thread 모듈 위에 더 높은 수준의 스레딩 인터페이스를 구축해요.
가용성(Availability): WASI 아님(WebAssembly). 이 모듈은 WebAssembly에서는 작동하지 않거나 사용할 수 없어요.
본문
소개 (Introduction)
threading 모듈은 단일 프로세스 안에서 여러 스레드(프로세스의 더 작은 단위)를 동시에 실행하는 방법을 제공해요. 스레드의 생성과 관리를 가능하게 하여, 메모리 공간을 공유하면서 작업을 병렬로 실행할 수 있게 해줘요. 스레드는 파일 작업이나 네트워크 요청처럼 많은 시간을 외부 자원을 기다리는 데 쓰는 I/O 바운드(I/O-bound) 작업에서 특히 유용해요.
threading의 일반적인 사용 사례는 여러 작업을 동시에 처리할 수 있는 작업자 스레드 풀을 관리하는 것이에요. Thread로 스레드를 생성하고 시작하는 기본 예제:
import threading
import time
def crawl(link, delay=3):
print(f"crawl started for {link}")
time.sleep(delay) # 블로킹 I/O (네트워크 요청 시뮬레이션)
print(f"crawl ended for {link}")
links = ["https://python.org", "https://docs.python.org", "https://peps.python.org"]
# 각 링크에 대한 스레드 시작
threads = []
for link in links:
# `args`로 위치 인자, `kwargs`로 키워드 인자 전달
t = threading.Thread(target=crawl, args=(link,), kwargs={"delay": 2})
threads.append(t)
# 각 스레드 시작
for t in threads:
t.start()
# 모든 스레드가 끝날 때까지 대기
for t in threads:
t.join()
관련: concurrent.futures.ThreadPoolExecutor는 호출 스레드의 실행을 차단하지 않고 백그라운드 스레드로 작업을 밀어넣는 더 높은 수준의 인터페이스를 제공하며, 필요할 때 결과를 검색할 수 있어요.
CPython 구현 상세: CPython에서는 Global Interpreter Lock(GIL) 때문에 한 번에 하나의 스레드만 Python 코드를 실행할 수 있어요. 멀티코어 머신의 계산 자원을 더 잘 활용하려면 multiprocessing이나 concurrent.futures.ProcessPoolExecutor를 사용하는 것이 좋아요. 다만 여러 I/O 바운드 작업을 동시에 실행하려면 threading이 여전히 적절한 모델이에요.
GIL과 성능 고려사항
multiprocessing 모듈은 GIL을 우회하기 위해 별도의 프로세스를 사용하는 반면, threading 모듈은 단일 프로세스 안에서 작동하므로 모든 스레드가 같은 메모리 공간을 공유해요. 하지만 GIL은 CPU 바운드 작업에서 threading의 성능 이익을 제한하는데, 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있기 때문이에요. 그럼에도 스레드는 많은 시나리오에서 동시성을 달성하는 유용한 도구로 남아 있어요.
Python 3.13부터 free-threaded 빌드는 GIL을 비활성화해 스레드의 진정한 병렬 실행을 가능하게 할 수 있지만, 이 기능은 기본적으로 사용할 수 없어요(PEP 703 참고).
참조 (Reference)
이 모듈은 다음 함수들을 정의해요:
- threading.active_count() — 현재 살아있는
Thread객체의 수를 반환해요.enumerate()가 반환한 목록의 길이와 같아요. (activeCount는 더 이상 사용되지 않는 별칭) - threading.current_thread() — 호출자의 스레드 제어에 해당하는 현재
Thread객체를 반환해요. 호출자의 스레드가threading모듈을 통해 생성되지 않았다면 제한된 기능의 더미 스레드 객체가 반환돼요. (currentThread는 더 이상 사용되지 않는 별칭) - threading.excepthook(args, /) —
Thread.run()이 발생시킨 처리되지 않은 예외를 처리해요.args에는exc_type,exc_value,exc_traceback,thread속성이 있어요.exc_type이SystemExit이면 예외는 조용히 무시되고, 그렇지 않으면sys.stderr에 인쇄돼요. 사용자 정의 훅으로exc_value를 저장하면 참조 사이클을 만들 수 있으므로, 예외가 더 이상 필요하지 않을 때 명시적으로 지워 참조 사이클을 깨야 해요. - threading.excepthook —
threading.excepthook()의 원래 값을 보관해요. - threading.get_ident() — 현재 스레드의 '스레드 식별자'를 반환해요. 0이 아닌 정수예요. 스레드가 종료되고 다른 스레드가 생성되면 재사용될 수 있어요.
- threading.get_native_id() — 커널이 할당한 현재 스레드의 네이티브 정수 Thread ID를 반환해요. 0이 아닌 정수예요.
- threading.enumerate() — 현재 활성화된 모든
Thread객체의 목록을 반환해요. 데몬 스레드와current_thread()가 만든 더미 객체를 포함하고, 종료된 스레드와 아직 시작되지 않은 스레드는 제외해요. 메인 스레드는 종료되어도 항상 결과의 일부예요. - threading.main_thread() — 메인
Thread객체를 반환해요. 정상 조건에서 메인 스레드는 Python 인터프리터가 시작된 스레드예요. - threading.settrace(func) —
threading모듈에서 시작된 모든 스레드에 추적 함수를 설정해요. - threading.settrace_all_threads(func) —
threading모듈에서 시작된 모든 스레드와 현재 실행 중인 모든 Python 스레드에 추적 함수를 설정해요. - threading.gettrace() —
settrace()가 설정한 추적 함수를 가져와요. - threading.setprofile(func) / threading.setprofile_all_threads(func) — 프로파일 함수를 설정해요.
- threading.getprofile() —
setprofile()이 설정한 프로파일러 함수를 가져와요. - threading.stack_size([size]) — 새 스레드를 만들 때 사용되는 스레드 스택 크기를 반환해요. 선택적
size인자는 이후 생성되는 스레드에 사용할 스택 크기를 지정하며, 0(플랫폼 또는 구성 기본값) 또는 최소 32,768(32 KiB)의 양의 정수여야 해요.
상수: threading.TIMEOUT_MAX — 블로킹 함수(Lock.acquire(), RLock.acquire(), Condition.wait() 등)의 timeout 매개변수에 허용되는 최대 값이에요. 이보다 큰 timeout을 지정하면 OverflowError가 발생해요.
스레드 로컬 데이터 (Thread-local data)
스레드 로컬 데이터는 값이 스레드 특정적인 데이터예요. 스레드에 로컬로 두고 싶은 데이터가 있으면 local 객체를 만들고 그 속성을 사용해요:
>>> mydata = local()
>>> mydata.number = 42
>>> mydata.number
42
다른 스레드에서 데이터에 접근하면 다른 데이터를 얻어요. local 클래스를 하위 클래스로 만들어 기본값, 메서드, 초기화를 지원하는 사용자 정의 로컬 객체를 만들 수 있어요.
classthreading.local
스레드 로컬 데이터를 나타내는 클래스예요.
스레드 객체 (Thread objects)
Thread 클래스는 별도의 스레드 제어에서 실행되는 활동을 나타내요. 활동을 지정하는 방법은 두 가지예요: 생성자에 호출 가능 객체를 전달하거나, 하위 클래스에서 run() 메서드를 재정의하는 것. 스레드 객체가 생성되면 스레드의 start() 메서드를 호출해 활동을 시작해야 해요. 스레드의 활동이 시작되면 스레드는 '살아있는(alive)' 것으로 간주되며, run() 메서드가 종료되면 살아있지 않게 돼요.
다른 스레드는 스레드의 join() 메서드를 호출할 수 있어요. 이는 호출 스레드를 종료될 때까지 블록해요. 스레드는 이름을 가지며 name 속성으로 읽거나 바꿀 수 있어요. 스레드는 "데몬 스레드"(daemon thread)로 표시될 수 있는데, 이 플래그의 의미는 데몬 스레드만 남았을 때 전체 Python 프로그램이 종료된다는 것이에요. 데몬 스레드는 종료 시 급작스럽게 중지되므로, 자원이 제대로 해제되지 않을 수 있어요.
classthreading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None, context=None)
이 생성자는 항상 키워드 인자로 호출해야 해요. group은 미래 확장을 위해 예약되어 있으므로 None이어야 해요. target은 run() 메서드가 호출할 호출 가능 객체이고, name은 스레드 이름이며, args는 위치 인자 목록/튜플, kwargs는 키워드 인자 사전, daemon은 데몬 여부를 명시적 설정, context는 스레드를 시작할 때 사용하는 Context 값이에요.
주요 메서드와 속성:
start()— 스레드의 활동을 시작해요. 스레드 객체당 최대 한 번 호출해야 해요. 같은 스레드 객체에서 두 번 이상 호출하면RuntimeError가 발생해요.run()— 스레드의 활동을 나타내는 메서드예요. 하위 클래스에서 재정의할 수 있어요. 표준run()은 대상 인자로 생성자에 전달된 호출 가능 객체를 호출해요.join(timeout=None)— 스레드가 종료될 때까지 기다려요.timeout이 주어지면 초 단위의 부동소수점 수여야 해요.join()은 항상 None을 반환하므로, 시간 초과가 발생했는지 판단하려면join()후is_alive()를 호출해야 해요. 현재 스레드를 join하려 하면 교착 상태가 되므로RuntimeError를 발생시켜요. Python finalization의 후기 단계에서 실행 중인 데몬 스레드를 join하려 하면PythonFinalizationError를 발생시켜요.name— 식별 목적으로만 사용되는 문자열이에요. 의미는 없어요.ident— 이 스레드의 '스레드 식별자' 또는 아직 시작되지 않았다면 None.native_id— OS(커널)가 할당한 이 스레드의 Thread ID (TID). 0 이상의 정수 또는 스레드가 시작되지 않았다면 None.is_alive()— 스레드가 살아있는지 여부를 반환해요.daemon— 이 스레드가 데몬 스레드인지 여부를 나타내는 부울 값.start()전에 설정해야 해요.
Lock 객체 (Lock objects)
기본 잠금(primitive lock)은 잠겨 있을 때 특정 스레드가 소유하지 않는 동기화 기본 요소예요. Python에서 현재 사용 가능한 가장 낮은 수준의 동기화 기본 요소로, _thread 확장 모듈이 직접 구현해요. 기본 잠금은 "locked" 또는 "unlocked" 두 상태 중 하나에 있어요. acquire()와 release() 두 가지 기본 메서드가 있어요. 모든 메서드는 원자적으로 실행돼요.
classthreading.Lock
기본 잠금 객체를 구현하는 클래스예요. 스레드가 잠금을 획득하면, 해제될 때까지 후속 획득 시도가 차단되고, 어떤 스레드든 해제할 수 있어요.
acquire(blocking=True, timeout=-1)— 잠금을 획득해요(블로킹 또는 논블로킹). 성공하면 True, 실패하면(예: timeout 만료) False를 반환해요.release()— 잠금을 해제해요. 획득한 스레드가 아닌 어떤 스레드에서든 호출할 수 있어요. 잠금이 unlocked 상태에서 호출하면RuntimeError가 발생해요.locked()— 잠금이 획득되었으면 True를 반환해요.
RLock 객체 (Reentrant lock objects)
재진입 잠금(reentrant lock)은 같은 스레드가 여러 번 획득할 수 있는 동기화 기본 요소예요. 내부적으로 "소유 스레드"와 "재귀 수준(recursion level)" 개념을 사용해요. RLock의 acquire()/release() 호출 쌍은 중첩될 수 있어요. 마지막 release()만 잠금을 unlocked 상태로 재설정해요.
classthreading.RLock
재진입 잠금 객체를 구현해요. 재진입 잠금은 획득한 스레드가 해제해야 해요.
Condition 객체 (Condition objects)
조건 변수(condition variable)는 항상 어떤 종류의 잠금과 연관돼요. wait() 메서드는 잠금을 해제한 다음, 다른 스레드가 notify() 또는 notify_all()을 호출해 깨울 때까지 차단돼요. notify() 메서드는 조건 변수를 기다리는 스레드 중 하나(있다면)를 깨우고, notify_all()은 모두 깨워요. wait_for() 메서드는 조건 검사를 자동화해요.
classthreading.Condition(lock=None)
조건 변수 객체를 구현해요. lock 인자가 주어지지 않으면 새 RLock 객체가 생성되어 사용돼요.
acquire(*args)/release()— 기본 잠금 획득/해제wait(timeout=None)— 알림을 받거나 시간 초과가 발생할 때까지 기다려요. 호출 스레드가 잠금을 획득하지 않았으면RuntimeError가 발생해요.wait_for(predicate, timeout=None)— 조건이 true로 평가될 때까지 기다려요.notify(n=1)— 기본적으로 이 조건을 기다리는 스레드 하나를 깨워요.notify_all()— 이 조건을 기다리는 모든 스레드를 깨워요. (notifyAll은 더 이상 사용되지 않는 별칭)
세마포어 객체 (Semaphore objects)
세마포어는 각 acquire() 호출로 감소하고 각 release() 호출로 증가하는 내부 카운터를 관리해요. 카운터는 0보다 아래로 내려갈 수 없어요.
classthreading.Semaphore(value=1)
세마포어 객체를 구현해요. value가 0보다 작으면 ValueError가 발생해요.
acquire(blocking=True, timeout=None)— 세마포어를 획득해요.release(n=1)— 내부 카운터를n만큼 증가시켜 세마포어를 해제해요.
classthreading.BoundedSemaphore(value=1)
바운드 세마포어 객체를 구현하는 클래스예요. 현재 값이 초기 값을 초과하지 않도록 확인하며, 초과하면 ValueError가 발생해요. 대부분의 상황에서 세마포어는 제한된 용량의 자원을 보호하는 데 사용돼요. 세마포어가 너무 많이 해제되면 버그의 신호예요.
세마포어 예제
세마포어는 종종 데이터베이스 서버처럼 제한된 용량의 자원을 보호하는 데 사용돼요. 자원 크기가 고정된 상황에서는 바운드 세마포어를 사용해야 해요. 바운드 세마포어를 사용하면 세마포어가 획득한 것보다 더 많이 해제되는 프로그래밍 오류가 감지되지 않고 지나갈 가능성이 줄어들어요.
Event 객체 (Event objects)
이것은 스레드 사이 통신을 위한 가장 간단한 메커니즘 중 하나예요: 한 스레드가 이벤트를 신호하고 다른 스레드가 그것을 기다려요. 이벤트 객체는 set() 메서드로 true로, clear() 메서드로 false로 재설정할 수 있는 내부 플래그를 관리해요. wait() 메서드는 플래그가 true일 때까지 블록해요.
classthreading.Event
set(), clear(), is_set(), wait(timeout=None) 메서드를 가진 클래스예요.
Timer 객체 (Timer objects)
이 클래스는 일정 시간이 지난 후에만 실행되어야 하는 동작(타이머)을 나타내요. Timer는 Thread의 하위 클래스예요.
def hello():
print("hello, world")
t = Timer(30.0, hello)
t.start() # 30초 후 "hello, world"가 인쇄됨
classthreading.Timer(interval, function, args=None, kwargs=None)
interval 초가 지난 후 function을 args와 kwargs로 실행할 타이머를 만들어요. cancel() 메서드로 타이머를 중지할 수 있어요(동작이 시작되기 전에만 작동).
Barrier 객체 (Barrier objects)
이 클래스는 서로를 기다려야 하는 고정된 수의 스레드가 사용하는 간단한 동기화 기본 요소를 제공해요. 각 스레드는 wait() 메서드를 호출해 장벽을 통과하려 하며, 모든 스레드가 wait() 호출을 할 때까지 차단되다가 그 시점에 동시에 해제돼요. 장벽은 같은 수의 스레드에 대해 몇 번이든 재사용할 수 있어요.
classthreading.Barrier(parties, action=None, timeout=None)
parties 수의 스레드를 위한 장벽 객체를 만들어요. action은 해제될 때 스레드 중 하나가 호출할 호출 가능 객체예요.
wait(timeout=None)— 장벽을 통과해요. 반환값은 0에서 parties-1 사이의 정수로, 스레드마다 달라요. 시간 초과 시 장벽은 broken 상태가 돼요.reset()— 장벽을 기본의 빈 상태로 되돌려요.abort()— 장벽을 broken 상태로 만들어요.parties— 장벽을 통과하는 데 필요한 스레드 수n_waiting— 현재 장벽에서 기다리는 스레드 수broken— 장벽이 broken 상태이면 True인 부울
exceptionthreading.BrokenBarrierError
RuntimeError의 하위 클래스로, Barrier 객체가 재설정되거나 깨졌을 때 발생해요.
with 문에서 잠금, 조건, 세마포어 사용
이 모듈이 제공하는 acquire와 release 메서드를 가진 모든 객체는 with 문의 컨텍스트 매니저로 사용할 수 있어요. 블록에 들어갈 때 acquire가, 나올 때 release가 호출돼요. 현재 Lock, RLock, Condition, Semaphore, BoundedSemaphore 객체를 with 문 컨텍스트 매니저로 사용할 수 있어요.
with some_lock:
# do something...