threading — 스레드 기반 병렬 처리

threading — 스레드 기반 병렬 처리

threading 모듈은 저수준 스레드 모듈 위에 더 높은 수준의 스레드 인터페이스를 제공해요. 파이썬 3.x에서 이 모듈은 선택 사항이었던 dummy_threading 모듈과는 달리 항상 사용 가능해요. dummy_threading은 3.7부터 제거됐고, _thread 모듈이 파이썬이 스레드 없이 빌드될 때 사용 가능한지 여부를 제공합니다.

이 모듈은 파이썬의 저수준 _thread 모듈 위에 스레드, 잠금(Lock), 락(Rlock), 세마포어(Semaphore), 이벤트(Event), 조건(Condition), 타이머(Timer) 같은 동기화 프리미티브를 설계·구현해요. 이런 프리미티브들이 _thread 모듈의 것보다 더 강력하고 안전한 사용 방식을 제공하죠.

경고: 스레드 상호작용은 CPython에서 가장 섬세한 영역 중 하나라서, 견고하고 정확한 스레드 코드를 작성하는 일은 경험 많은 전문가조차 어렵게 만듭니다. 여기 문서는 그런 복잡성을 깊이 다루지 않아요. 자세한 내용은 concurrent.futures, multiprocessing, queue, asyncio 문서의 관련 절과 관련 서적(C++/Java 스레딩 서적도 여전히 매우 유용해요)을 참고하세요.

출처: Python 표준 라이브러리 — threading

본문

스레드 데이터 (Thread Data)

threading.local()

스레드-로컬 데이터를 나타내는 로컬(local) 클래스 인스턴스를 반환해요. 로컬 인스턴스의 속성은 여러 스레드마다 다를 수 있고, 어떤 스레드의 코드라도 해당 스레드의 값을 읽고 씁니다.

이 클래스는 _threading_local 모듈을 기반으로 구현돼요. 각 인스턴스마다 별도의 잠금 대신, 프로세스당 단일 잠금이 사용되는데, 이 잠금은 인스턴스 속성에 대한 접근의 시작과 끝에서 각각 획득·해제되는 락입니다. 지연 초기화(lazy initialization)로 성능 부하를 낮춰요. 즉 잠금은 인스턴스 속성을 읽거나 쓸 때만 획득됩니다. 관리되는(class-managed) 속성만 잠금으로 보호되고, 인스턴스 내부 딕셔너리는 잠금으로 보호되지 않아요.

class threading.local

로컬 인스턴스의 메서드 정의는 별도 모듈에서 no-op인 경우를 제외하고는 AttributeError를 발생시켜요. 로컬 인스턴스의 하위 클래스화는 기본적으로 지원되지 않으며, __init____new__처럼 __slots__을 벗어난 특수 메서드 정의도 지원되지 않아요. 로컬 인스턴스의 하위 클래스화가 필요한 애플리케이션에선 _threading_local.local을 쓰세요. 로컬 인스턴스는 __slots__을 지원하지 않아서, 로컬 인스턴스의 하위 클래스도 __slots__을 가질 수 없어요. 로컬 인스턴스의 속성이 없으면 AttributeError 예외를 발생시켜 봐요.

스레드 객체 (Thread Objects)

threading 모듈은 함수 실행을 별도의 스레드로 하는 가장 간단한 인터페이스를 제공해요.

threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

target이 주어지면 target(*args, **kwargs)로 호출되는 별도의 제어 스레드를 시작해요. group은 예약돼 있고 향후 확장용이에요.

name이 주어지면 스레드를 구분하는 데 쓰이고, 그렇지 않으면 Thread-N 형식으로 자동 생성돼요.

하위 클래스에서 __init__()을 재정의(override)한다면, 다른 메서드를 실행하기 전에 반드시 Thread.__init__(self)를 호출해야 해요.

daemonNone(기본)이면 이 플래그는 현재 스레드로부터 상속되고, None이 아니면 명시적으로 설정돼요.

참고: daemon 플래그가 스레드 시작 후(즉 start() 호출 후)에도 유효한지에 대해서는 아래의 daemon 설명을 보세요.

start() — 스레드 활동을 시작해요. 각 스레드 객체마다 최대 한 번만 호출해야 하고, 실행이 진행 중인 스레드에 다시 호출되면 RuntimeError가 발생해요.

run() — 이 스레드의 활동을 나타내는 메서드예요. 생성자에 target 인자를 넘겨 대상으로 지정한 함수를 호출합니다. 하위 클래스에서 이 메서드를 재정의할 수 있어요.

join(timeout=None) — 이 스레드가 종료될 때까지 기다려요. timeout 인자가 주어지면 None 또는 부동 소수점 초로, 이 시간만 기다린 뒤 리턴해요. timeout이 주어지지 않으면 스레드가 종료될 때까지 블록합니다. 어떤 스레드든 join()될 수 있다는 걸 이해하는 게 중요해요 — 예를 들어 한 스레드가 다른 스레드를 join하면 둘 다 중간에 닫혀도 안전해요. 호출 스레드가 target 스레드와 같은 스레드면(자기 자신을 join하려 하면) RuntimeError가 발생해요.

name — 스레드를 식별하기 위한 문자열. 이름은 생성자에 전달하거나 name 속성에 할당해서 설정할 수 있어요. 기본적으로 Thread-N 형식입니다.

getName() / setName(name) — 각각 이름을 반환하고 설정하는 예전 API. 이들은 name 속성에 대한 접근자/설정자로, Python 3 로컬 네임에서도 getName·setName으로 불립니다.

ident — 이 스레드의 식별자, 또는 아직 시작되지 않았으면 None. 이는 양의 정수 또는 None이에요. start()가 호출되면 이 식별자가 실제 스레드 ID에 할당돼요. 스레드가 종료되고 다른 스레드가 생성되면 ident가 재사용될 수 있어요.

native_id — OS가 할당한 이 스레드의 (native) 스레드 식별자, 또는 아직 시작되지 않았으면 None. 이 값은 OS(예: Linux의 gettid()와 Windows의 GetCurrentThreadId())에서 검색되는 음수 아닌 정수예요. get_native_id() 메서드로 검색할 수 있어요. / 버전 3.8에서 추가.

is_alive() — 스레드가 살아 있는지(active)를 반환해요. start() 호출 이후부터 run() 메서드가 종료될 때까지 참입니다. run() 메서드가 종료된 후에는 False를 반환해요.

daemon — 이 스레드가 데몬 스레드인지 여부. True면 이 파이썬 프로그램은 데몬이 아닌 스레드가 모두 종료됐을 때 데몬 스레드의 실행을 기다리지 않고 종료돼요. 기본값은 부모 스레드에서 상속된 값입니다.

참고: 스레드가 시작된 후 daemon을 반드시 고정해야 해요. 이 플래그를 start() 호출 후에 설정하면 RuntimeError가 발생합니다.

버전 3.3부터 변경: 이전에는 시작 전에만 설정할 수 있었어요.

stack_size([size]) — 새 스레드를 만들 때 사용할 스택 크기. size를 지정하면 새 스레드 생성에 사용할 스택 크기를 바이트 단위로 설정하고, 지정하지 않으면 현재 설정을 반환해요.

이 메서드는 초기화된 RLock 인스턴스처럼 동작합니다.

기타: isDaemon(), setDaemon(daemonic), isAlive() — 파이썬 2.x의 별칭 메서드들이에요. isAlive()의 반환값은 is_alive()와 같고, 나머지 둘은 각각 daemon 속성의 접근자/설정자예요. 이 별칭들은 3.14에서 폐기(deprecated)됩니다.

잠금 객체 (Lock Objects)

프리미티브 잠금(primitive lock)은 기본적으로 간단한 동기화 프리미티브로, 잠금이 해제되었을 때 진입하고(acquire), 즉 한 번에 하나의 스레드만 잠금을 소유합니다. 두 개 이상의 스레드가 잠금을 진입하려 하면 그중 하나만 성공하고 나머지는 잠금이 해제될 때까지 기다려요. 잠금은 소유자가 없습니다(잠금은 아무 스레드나 release 호출할 수 있음). 프리미티브 잠금은 Lock 클래스를 인스턴스화해서 생성할 수 있어요.

lock.acquire(blocking=True, timeout=-1) — 잠금을 획득합니다. blocking 인자가 True면 잠금이 해제될 때까지 블록하고, False면 즉시 반환돼요.

lock.release() — 잠금을 해제해요. 잠금이 잠겨 있지 않으면 RuntimeError가 발생합니다.

lock.locked() — 잠금이 잠겨 있으면 True를 반환해요.

RLock 재귀 잠금

재귀 잠금(recursive lock)은 같은 스레드가 잠금을 다시 획득할 수 있는 잠금 프리미티브예요.

rlock.acquire(blocking=True, timeout=-1) — 잠금을 획득합니다. blockingTrue면 잠금이 해제될 때까지 블록합니다. 잠금을 소유한 스레드가 이 메서드를 호출하면 잠금의 재귀 수준(recursion level)이 1 증가하고 즉시 반환돼요. 잠금이 해제 상태이거나 재귀 수준이 0이면 재귀 수준이 1로 설정됩니다. blockingFalse면 잠금을 획득할 수 있을 때만 True를 반환하고, 그렇지 않으면 False를 반환해요.

rlock.release() — 잠금을 해제합니다. 재귀 수준이 1보다 크면 재귀 수준이 1 감소하고, 1이면 잠금이 해제됩니다.

rlock.locked() — 잠금이 잠겨 있으면 True를 반환해요. 잠금이 해제 상태여도 True를 반환하므로 사용에 주의해야 해요.

조건 객체 (Condition Objects)

class threading.Condition(lock=None) — 조건 변수는 항상 어떤 잠금과 연관돼요. 이 잠금은 명시적으로 전달하거나, 기본적으로 생성되는 RLock 인스턴스를 쓸 수 있어요. lock 인자로 generic Lock을 주면 Condition의 wait/notify 연산에 잠금이 사용됩니다.

condition.acquire(*args) — 연관 잠금을 획득해요. condition.release() — 연관 잠금을 해제해요. condition.wait(timeout=None) — 통지받을 때까지 또는 timeout이 만료될 때까지 기다려요. condition.wait_for(predicate, timeout=None) — predicate가 참이 될 때까지 기다려요. condition.notify(n=1) — 이 조건을 기다리는 최대 n개의 스레드를 깨워요. condition.notify_all() — 이 조건을 기다리는 모든 스레드를 깨워요.

세마포어 객체 (Semaphore Objects)

class threading.Semaphore(value=1) — 세마포어는 내부 카운터를 관리하는 객체예요. 카운터가 0보다 크면 진입(acquire)을 감소시키고 즉시 반환하며, 카운터가 0이면 카운터가 증가할 때까지 블록합니다.

class threading.BoundedSemaphore(value=1) — 획득 횟수가 초과하지 않는(즉 초기 값 이하로) 세마포어. 카운터가 초기 값보다 높으면 ValueError를 발생시켜요.

semaphore.acquire(blocking=True, timeout=None), semaphore.release(), semaphore.locked()

이벤트 객체 (Event Objects)

class threading.Event() — 스레드 간 신호 전달을 위한 기본 구현. 이벤트 객체는 내부 플래그와 스레드를 기다리게 하는 잠금을 관리해요. is_set()로 조회, set()으로 설정, clear()로 해제하고, wait() 메서드로 블록합니다.

타이머 객체 (Timer Objects)

class threading.Timer(interval, function, args=None, kwargs=None) — 일정 시간이 지난 후에 간단한 함수를 실행하려는 조정 클래스. TimerThread의 하위 클래스이며, 자신의 생성자에 정의된 지연 시간 후 함수를 실행하기 위한 run() 메서드를 제공합니다.

timer.cancel() — 타이머를 중지하고 타이머 실행이 예약된 동작을 취소해요. cancel()이 반환된 후에 타이머가여전히 종료될 수는 있지만(예: 함수가 이미 실행 중인 경우), 취소된 타이머는 절대 실행되지 않아요.

배리어 객체 (Barrier Objects)

class threading.Barrier(parties, action=None, timeout=None) — 여러 스레드가 각자의 어떤 지점에 도달할 때까지 서로 기다리게 하는 동기화 프리미티브예요. 배리어는 반복 사용 가능(reusable)해요 — 스레드가 배리어에 도달하면 그들은 release되고 다음 라운드에 다시 사용할 수 있어요. parties는 참여 스레드 수를 지정하고, action은 스레드가 release되면 제공된 호출 가능 함수로 바리어에 의해 호출됩니다.

기타 (Miscellaneous)

threading.daemon_threads, threading.excepthook(args, /), record_lock 컨텍스트 매니저, get_ident(), enumerate(), active_count(), current_thread(), main_thread(), setprofile(func), settrace(func), setprofile_all_threads(func), settrace_all_threads(func), getprofile(), gettrace(), set_symlink_name(name), threading.TIMEOUT_MAX 등 다양한 모듈 수준 함수·상수가 더 있어요.

락 사용과 with 구문

Lock·RLock·Condition·Semaphore·BoundedSemaphore·Event 객체는 with 문에서 사용할 수 있어서, with lock: 블록에 진입할 때 잠금을 획득하고 블록이 끝날 때 잠금을 해제합니다. Condition.acquire()blocking·timeout을 무시하지 않고 그대로 전달하므로 with 블록에서는 항상 잠금을 획득합니다.

예시 (Examples)

다음 예는 각 스레드가 자신의 사용자 이름을 가진 로컬 학습자(learner)를 가지는 예시예요.

import threading

def show_current_data():
    # 'data' is a thread-local object
    print(threading.current_thread().name, getattr(data, 'value', 'not set'))

data = threading.local()
for i in range(1, 4):
    t = threading.Thread(target=lambda: setattr(data, 'value', i))
    t.start()

import threading

def worker(i):
    """thread worker function"""
    print('Worker', i)
    return

threads = []
for i in range(5):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)
    t.start()

Lock을 with문으로 쓰는 전형적인 예:

import threading

shared_state_lock = threading.Lock()
shared_state = {"count": 0}

def update():
    global shared_state
    with shared_state_lock:
        shared_state["count"] += 1
        print(f"count updated to {shared_state['count']}")

타이머 사용 예시:

import threading
import time

def hello():
    print("hello, world")

t = threading.Timer(30.0, hello)
t.start()  # after 30 seconds, "hello, world" will be printed

더 알아보기