파이썬 표준 라이브러리의 _thread 모듈은 여러 스레드가 하나의 전역 데이터 공간을 공유하는 경량 프로세스를 다룰 수 있게 해주는 저수준(low-level) 인터페이스를 제공합니다. 이 모듈에는 스레드 간 동기화를 위한 간단한 잠금(lock) 객체, 즉 흔히 뮤텍스(mutex) 또는 바이너리 세마포어(binary semaphore)라고 불리는 도구가 정의되어 있습니다. 참고로 파이썬에 내장된 threading 모듈은 이 _thread 모듈 위에 구축된 더 높은 수준의 스레딩 API를 제공하므로, 실무 환경에서는 가급적 threading 모듈을 사용하는 것이 권장됩니다.
start_new_thread() 함수
start_new_thread()는 현재 프로세스 안에서 새로운 스레드를 시작하는 모듈 수준의 함수입니다. 이 함수는 함수 객체를 인자로 받으며, 새 스레드가 성공적으로 생성되면 해당 함수가 호출됩니다. 이때 전달된 함수의 실행 범위는 곧 스레드의 생명 주기와 일치합니다. 필요하다면 sleep() 함수를 호출해 스레드의 실행을 잠시 멈출 수도 있습니다.
다음은 _thread 모듈을 사용한 기본적인 스레드 동작의 간단한 예제입니다.
import _thread
import time
def run(threadName):
for i in range(1, 6):
time.sleep(5)
print(threadName, i)
_thread.start_new_thread(run, ("child", ))
for i in range(1, 6):
print('main', i)
time.sleep(5)
start_new_thread() 함수가 새 스레드를 생성하여 run 함수를 병렬로 호출합니다. run() 함수 내부와 프로그램의 메인 스레드에 각각 반복문이 존재하며, 양쪽에서 호출한 sleep() 함수 덕분에 두 스레드의 실행이 아래와 같이 서로 겹쳐서 진행됩니다.
main 1 child 1 main 2 child 2 main 3 child 3 main 4 child 4 main 5 child 5
Lock 객체를 이용한 스레드 동기화
스레드 간 동기화는 Lock 객체를 통해 구현할 수 있습니다. allocate_lock() 함수를 호출하면 lock 객체가 반환되며, 이 객체에는 다음과 같은 메서드들이 제공됩니다.
acquire()
이 메서드는 다른 스레드가 잠금을 해제할 때까지 해당 잠금을 무조건적으로 획득합니다. 한 번에 오직 하나의 스레드만 잠금을 획득할 수 있으며, 획득에 성공하면 True, 실패하면 False를 반환합니다.
release()
이 메서드는 잠금을 해제합니다. 잠금은 이전에 획득된 상태여야 하지만, 반드시 획득했던 동일한 스레드가 해제할 필요는 없습니다.
아래 예제에서는 두 개의 스레드를 선언하고, 각 스레드가 run() 함수를 동시에 호출하도록 합니다. 먼저 잠금을 획득한 스레드가 'synchronized' 함수에 진입해 작업을 수행하고, 나머지 스레드는 잠금이 해제될 때까지 대기하게 됩니다.
import _thread
import time
def run(threadName):
lock.acquire()
synchronized(threadName)
lock.release()
def synchronized(threadName):
print(threadName, "has acquired lock")
counter = 10
while counter:
time.sleep(1)
print('*', end='')
counter = counter - 1
print('\n{} has released lock'.format(threadName))
lock = _thread.allocate_lock()
_thread.start_new_thread(run, ("t1", ))
_thread.start_new_thread(run, ("t2", ))
실행 결과
>>> t1 has acquired lock ********** t1 has released lock t2 has acquired lock ********** t2 has released lock
실행 결과를 보면 t1 스레드가 먼저 잠금을 획득해 작업을 모두 마친 뒤 잠금을 해제하고, 그 후에야 t2 스레드가 잠금을 획득해 자신의 작업을 수행하는 것을 확인할 수 있습니다. 이처럼 Lock 객체를 활용하면 여러 스레드가 공유 자원에 동시에 접근해 발생할 수 있는 경쟁 상태(race condition)를 효과적으로 방지할 수 있습니다.