본문 바로가기
Programming/Python

Python Thread 사용 시 주의해야할 점

by 느리게 걷는 즐거움 2024. 5. 17.
반응형

파이썬 스레드 사용 시 주의해야 할 점

파이썬에서 스레드를 사용하면 여러 작업을 동시에 처리할 수 있지만, 스레드를 사용할 때 몇 가지 주의할 점이 있습니다. 이러한 주의사항을 알고 적절히 대처하는 것이 중요합니다.

Global Interpreter Lock (GIL)

파이썬 인터프리터는 GIL이라는 메커니즘으로 여러 스레드가 동시에 실행되는 것을 제한합니다. 따라서 CPU-bound 작업을 처리하는 경우에는 멀티스레딩이 병목 현상을 유발할 수 있습니다. 이런 경우에는 멀티프로세싱이나 비동기 프로그래밍을 고려해야 합니다.

스레드 간 동기화

여러 스레드가 동시에 공유 자원에 접근할 때는 데이터 무결성 문제가 발생할 수 있습니다. 이를 해결하기 위해 Lock, Semaphore, Condition 등의 동기화 메커니즘을 사용하여 스레드 간에 상호 배제를 구현해야 합니다.

파이썬에서 스레드 간 동기화는 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지하기 위해 필요합니다. 동기화를 통해 데이터 무결성을 유지하고, 예상치 못한 동작을 피할 수 있습니다. 스레드 간 동기화를 구현하는 방법에는 여러 가지가 있으며, 대표적인 몇 가지를 소개하겠습니다.

Lock (잠금)

Lock은 가장 기본적인 동기화 도구입니다. 한 번에 하나의 스레드만 특정 코드 블록을 실행할 수 있도록 합니다. Lock 객체를 사용하면 스레드 간에 자원 접근을 제어할 수 있습니다.

import threading

# 공유 자원
shared_resource = 0

# Lock 객체 생성
lock = threading.Lock()

def thread_task():
    global shared_resource
    for _ in range(100000):
        # 잠금 획득
        lock.acquire()
        shared_resource += 1
        # 잠금 해제
        lock.release()

# 스레드 생성
threads = []
for _ in range(10):
    thread = threading.Thread(target=thread_task)
    threads.append(thread)
    thread.start()

# 모든 스레드가 종료될 때까지 대기
for thread in threads:
    thread.join()

print("Final value of shared_resource:", shared_resource)
RLock (재진입 잠금)

RLock(재진입 잠금)은 동일한 스레드가 여러 번 획득할 수 있는 잠금입니다. 재귀적으로 호출되는 함수나 메서드에서 유용합니다.

import threading

# RLock 객체 생성
rlock = threading.RLock()

def recursive_function(n):
    rlock.acquire()
    print("Acquired lock, n =", n)
    if n > 0:
        recursive_function(n - 1)
    rlock.release()
    print("Released lock, n =", n)

# 스레드 생성 및 시작
thread = threading.Thread(target=recursive_function, args=(5,))
thread.start()
thread.join()
Semaphore (세마포어)

Semaphore는 내부 카운터를 사용하여 여러 스레드가 지정된 최대 개수만큼 공유 자원에 접근할 수 있도록 합니다.

import threading
import time

# Semaphore 객체 생성, 최대 3개의 스레드가 접근 가능
semaphore = threading.Semaphore(3)

def worker(num):
    semaphore.acquire()
    print(f"Worker {num} is working")
    time.sleep(2)
    print(f"Worker {num} is done")
    semaphore.release()

# 스레드 생성 및 시작
threads = []
for i in range(10):
    thread = threading.Thread(target=worker, args=(i,))
    threads.append(thread)
    thread.start()

# 모든 스레드가 종료될 때까지 대기
for thread in threads:
    thread.join()
Condition (조건 변수)

Condition 객체는 특정 조건을 기다리는 스레드들을 위해 사용됩니다. wait()와 notify(), notify_all() 메서드를 통해 동기화할 수 있습니다.

import threading

# Condition 객체 생성
condition = threading.Condition()
shared_resource = []

def producer():
    global shared_resource
    with condition:
        for i in range(5):
            shared_resource.append(i)
            print("Produced", i)
            condition.notify()  # 소비자에게 알림
            condition.wait()  # 다음 생산을 기다림

def consumer():
    global shared_resource
    with condition:
        while True:
            condition.wait()  # 생산자가 알림을 줄 때까지 기다림
            if shared_resource:
                item = shared_resource.pop(0)
                print("Consumed", item)
                condition.notify()  # 생산자에게 알림

# 스레드 생성 및 시작
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

producer_thread.start()
consumer_thread.start()

# 스레드 종료 대기
producer_thread.join()
consumer_thread.join()
Event (이벤트)

Event 객체는 하나 이상의 스레드가 이벤트가 설정될 때까지 기다릴 수 있도록 합니다. set(), clear(), wait() 메서드를 사용합니다.

import threading
import time

# Event 객체 생성
event = threading.Event()

def worker():
    print("Worker waiting for event to be set")
    event.wait()  # 이벤트가 설정될 때까지 대기
    print("Worker received the event")

# 스레드 생성 및 시작
thread = threading.Thread(target=worker)
thread.start()

time.sleep(2)
print("Setting the event")
event.set()  # 이벤트 설정

# 스레드 종료 대기
thread.join()

데드락(Deadlock)

두 개 이상의 스레드가 서로 상대방의 작업이 끝나기를 기다리면서 무한 대기 상태에 빠지는 현상입니다. 데드락을 방지하기 위해 스레드 간에 동시에 여러 자원을 사용하는 경우에는 자원에 대한 접근 순서를 고려하여 데드락을 방지해야 합니다.

데드락의 조건

데드락이 발생하기 위해서는 다음 네 가지 조건이 동시에 만족되어야 합니다. 이를 "코피(Koffi) 조건" 또는 "데드락의 4가지 조건"이라고 합니다.

상호 배제 (Mutual Exclusion): 자원은 한 번에 하나의 프로세스만 사용할 수 있습니다. 자원이 공유 불가능한 경우, 다른 프로세스는 해당 자원을 사용할 수 없게 됩니다.

점유와 대기 (Hold and Wait): 최소한 하나의 프로세스가 자원을 점유하고 있으며, 추가 자원을 요청하면서 그 자원이 해제되기를 기다리는 상태입니다.

비선점 (No Preemption): 이미 할당된 자원을 강제로 빼앗을 수 없습니다. 자원을 점유하고 있는 프로세스만이 자원을 자발적으로 해제할 수 있습니다.

순환 대기 (Circular Wait): 대기 중인 프로세스들이 원형으로 자원을 기다리는 상태입니다. 예를 들어, P1이 P2의 자원을 기다리고, P2가 P3의 자원을 기다리고, P3가 P1의 자원을 기다리는 상황입니다.

import threading

# 두 개의 잠금 객체 생성
lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_routine():
    with lock1:
        print("Thread 1 acquired lock 1")
        with lock2:
            print("Thread 1 acquired lock 2")

def thread2_routine():
    with lock2:
        print("Thread 2 acquired lock 2")
        with lock1:
            print("Thread 2 acquired lock 1")

# 두 개의 스레드 생성 및 시작
t1 = threading.Thread(target=thread1_routine)
t2 = threading.Thread(target=thread2_routine)

t1.start()
t2.start()

t1.join()
t2.join()

위 코드에서는 thread1_routine이 lock1을 획득한 후 lock2를 기다리고, thread2_routine이 lock2를 획득한 후 lock1을 기다리기 때문에 데드락이 발생합니다.

데드락 예방 및 회피

데드락을 예방하고 회피하는 방법에는 여러 가지가 있습니다.

잠금 순서 지정(Lock Ordering):

모든 스레드가 자원을 획득하는 순서를 동일하게 유지하면 데드락을 피할 수 있습니다.

def thread1_routine():
    with lock1:
        print("Thread 1 acquired lock 1")
        with lock2:
            print("Thread 1 acquired lock 2")

def thread2_routine():
    with lock1:
        print("Thread 2 acquired lock 1")
        with lock2:
            print("Thread 2 acquired lock 2")
타임아웃 사용(Try Lock with Timeout)

acquire() 메서드에 타임아웃을 설정하여 스레드가 일정 시간 동안만 기다리도록 할 수 있습니다.

if lock1.acquire(timeout=1):
    try:
        print("Thread 1 acquired lock 1")
        if lock2.acquire(timeout=1):
            try:
                print("Thread 1 acquired lock 2")
            finally:
                lock2.release()
    finally:
        lock1.release()
교착 상태 회피 알고리즘

자원을 할당할 때 시스템의 상태를 추적하고 교착 상태를 피하는 알고리즘을 사용할 수 있습니다. 대표적인 방법으로 은행가 알고리즘이 있습니다.

 

스레드 풀(Thread Pool) 사용

많은 스레드를 생성하면 스레드 생성 및 소멸에 따른 오버헤드가 발생할 수 있습니다. 따라서 필요한 경우에는 스레드 풀을 사용하여 스레드의 재사용을 고려해야 합니다.

에러 핸들링

스레드 내에서 발생하는 예외는 해당 스레드에서 처리되지 않으면 메인 스레드로 전파되지 않습니다. 따라서 스레드 내에서 발생하는 예외를 적절히 처리하여 프로그램의 안정성을 유지해야 합니다.

이러한 주의사항을 염두에 두고 스레드를 사용하면 파이썬으로 작성한 프로그램을 보다 안전하고 효율적으로 만들 수 있습니다.


반응형

'Programming > Python' 카테고리의 다른 글

Python 라이브러리 백업/복원  (0) 2024.05.25
주피터 노트북 외부에서 실행하기  (0) 2024.05.23
Python Thread 사용방법  (0) 2024.05.17
Cython: Python 성능 향상  (0) 2024.05.17
Python에서 정규식사용하기  (0) 2024.05.17