Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python 저수준 스레딩 API: _thread 모듈로 멀티스레드 프로그래밍하기

파이썬 표준 라이브러리의 _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)를 효과적으로 방지할 수 있습니다.