파이썬 멀티스레딩 기초 및 실습 가이드

threading 모듈의 핵심 개념

파이썬의 threading 모듈은 동시성 처리를 위한 기본 도구로, 프로세스와 유사한 인터페이스를 제공합니다. 주로 단일 프로세스 내에서 여러 스레드를 생성하여 작업을 병렬로 수행할 수 있도록 합니다.

스레드 생성 방법 두 가지

스레드는 함수 기반과 클래스 기반으로 생성할 수 있습니다.

  • 함수 기반: Thread(target=function, args=()) 형태로 사용
  • 클래스 기반: Thread 상속 후 run() 메서드 오버라이드

예시: 함수 기반 스레드 생성

from threading import Thread
import time

def greet(name):
    time.sleep(1)
    print(f"{name} 안녕하세요")

if __name__ == "__main__":
    thread = Thread(target=greet, args=("김철수",))
    thread.start()
    print("메인 스레드 실행 중...")

예시: 클래스 기반 스레드 생성

from threading import Thread
import time

class GreetingThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    
    def run(self):
        time.sleep(1)
        print(f"{self.name} 인사 완료")

if __name__ == "__main__":
    worker = GreetingThread("이영희")
    worker.start()
    print("메인 스레드 종료 대기")

스레드와 프로세스의 차이점

동일한 프로세스 내에서 스레드는 공유된 메모리 공간을 사용하지만, 프로세스는 독립된 메모리 영역을 갖습니다.

스레드 생성 속도 비교

from threading import Thread
from multiprocessing import Process
import os

def task():
    print(f"작업 실행 - 프로세스 ID: {os.getpid()}")

if __name__ == "__main__":
    # 스레드 생성
    t = Thread(target=task)
    t.start()
    print("메인 스레드 완료")

    # 프로세스 생성
    p = Process(target=task)
    p.start()
    print("메인 프로세스 완료")

공유 데이터의 특성

from threading import Thread

counter = 100

def decrease():
    global counter
    temp = counter
    counter = temp - 1

if __name__ == "__main__":
    threads = []
    for _ in range(50):
        t = Thread(target=decrease)
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    print(f"최종 값: {counter}")  # 예상값: 50 (실제는 비결정적)

스레드 제어 및 상태 확인

  • is_alive(): 스레드가 실행 중인지 확인
  • getName(), setName(): 스레드 이름 설정 및 조회
  • threading.current_thread(): 현재 실행 중인 스레드 객체 반환
  • threading.enumerate(): 현재 활성화된 스레드 목록 반환
  • threading.active_count(): 실행 중인 스레드 수 반환

스레드 종료 대기 (join)

from threading import Thread
import time

def worker():
    time.sleep(2)
    print("작업 완료")

if __name__ == "__main__":
    thread = Thread(target=worker)
    thread.start()
    thread.join()  # 메인 스레드는 이 스레드 종료까지 대기
    print("메인 스레드 종료")

데몬 스레드 (Daemon Thread)

데몬 스레드는 메인 스레드 종료 시 자동으로 종료됩니다. 반드시 setDaemon(True)start() 전에 설정해야 합니다.

from threading import Thread
import time

def background_task():
    time.sleep(3)
    print("백그라운드 작업 완료")

if __name__ == "__main__":
    daemon_thread = Thread(target=background_task)
    daemon_thread.setDaemon(True)
    daemon_thread.start()
    print("메인 스레드 종료")
    # 백그라운드 스레드는 메인 종료 후 즉시 종료됨

GIL (Global Interpreter Lock) 이해

파이썬은 한 번에 하나의 스레드만 실행하도록 보장하는 GIL을 사용합니다. 이는 멀티스레딩 성능을 제한하지만, 간단한 동기화를 위해 필요합니다.

동기화 락 (Lock)

공유 자원 접근을 보호하기 위해 threading.Lock()을 사용합니다.

from threading import Thread, Lock

shared_value = 0
lock = Lock()

def increment():
    global shared_value
    with lock:
        temp = shared_value
        shared_value = temp + 1

if __name__ == "__main__":
    threads = [Thread(target=increment) for _ in range(10)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    
    print(f"최종 값: {shared_value}")  # 항상 10 출력

데드락과 재입장 가능 락 (RLock)

자신이 이미 잠금을 획득한 상태에서 다시 잠금을 시도하면 발생하는 데드락을 방지하기 위해 RLock 사용.

from threading import RLock

rlock = RLock()

def nested_function():
    with rlock:
        with rlock:  # 가능! RLock는 재진입 허용
            print("중첩 잠금 성공")

if __name__ == "__main__":
    thread = Thread(target=nested_function)
    thread.start()
    thread.join()

Event 객체를 통한 스레드 조절

Event는 특정 조건이 만족될 때까지 스레드를 일시 정지시키는 데 사용됩니다.

from threading import Thread, Event
import time

event = Event()

def wait_for_signal():
    print("기다리는 중...")
    event.wait()  # 이벤트가 설정될 때까지 대기
    print("신호 수신! 작업 시작")

def send_signal():
    time.sleep(2)
    event.set()  # 신호 발송
    print("신호 전송 완료")

if __name__ == "__main__":
    t1 = Thread(target=wait_for_signal)
    t2 = Thread(target=send_signal)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

concurrent.futures를 활용한 스레드/프로세스 풀

작업을 큐에 넣고 자동으로 관리하는 고급 패턴입니다.

from concurrent.futures import ThreadPoolExecutor
import time

def compute(n):
    time.sleep(1)
    return n * n

if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(compute, i) for i in range(5)]
        for future in futures:
            result = future.result()
            print(f"결과: {result}")

태그: python threading concurrency GIL Lock

7월 5일 23:23에 게시됨