Python Threading Module – Thread(3)

이전 글 Python Threading Module – Thread(2)
이번 글은 Python Documentation을 참고해서 작성하였다.

3. Class Thread의 메서드(method)

Thread 클래스의 주요 메서드는 다음과 같다.

  • start() – 스레드를 실행한다. 스레드 객체 당 최대 한 번만 호출할 수 있으며, 두 번 이상 호출하면 RuntimeError가 발생한다.
  • run() – 스레드의 실행을 나타내는 메서드로, 서브 클래스에서 이 메서드를 재정의할 수 있다.
  • join(timeout=None) – 스레드가 종료될 때까지 기다린다. 스레드가 정상 종료되거나 예외가 발생하거나, timeout 초가 지나면 대기를 종료한다.
  • is_alive() – 스레드가 실행 중인지 확인한다.

여기서 start()run()은 비슷해 보이지만, 차이점은 자신만의 호출 스택을 생성하는지 여부에 있다.


run()과 start()의 차이

다음 그림을 통해 살펴보면, main 스레드에서 start() 메서드를 호출하면 새로운 호출 스택이 생성되고, run() 메서드는 해당 새로운 스택에 쌓인다. 이후 start()는 종료되고, 이후 main과 서브 스레드가 번갈아 실행되면서 스레드가 동작한다.

반면에 run() 메서드만 호출하면, 기존의 호출 스택에서 일반 함수처럼 실행되므로 스레드의 독립성이 보장되지 않는다.
따라서 스레드를 적용하려면 run()이 아닌 start() 메서드를 사용해야 한다.

아래는 run() 메서드를 직접 호출한 예제이다.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name  # 스레드 이름 지정

    def run(self):
        print("sub thread start", threading.currentThread().getName())
        time.sleep(3)
        print("sub thread end", threading.currentThread().getName())

for i in range(5):
    name = "thread {}".format(i)
    t = Worker(name)  # 서브 스레드 생성
    t.run()         # 서브 스레드의 run 메서드 호출

아래는 start() 메서드를 호출한 예제이다.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name  # 스레드 이름 지정

    def run(self):
        print("sub thread start", threading.currentThread().getName())
        time.sleep(3)
        print("sub thread end", threading.currentThread().getName())

for i in range(5):
    name = "thread {}".format(i)
    t = Worker(name)  # 서브 스레드 생성
    t.start()         # 서브 스레드의 start 메서드 호출

실행 결과를 보면, run()을 직접 호출한 경우는 메인 스레드와 동일한 호출 스택에서 실행되므로, 하나의 실행이 끝나야 다음 실행이 진행된다. 즉, threading.currentThread().getName()의 결과로 MainThread가 출력된다.
반면, start()를 호출하면 5개의 스레드가 각각 독립적으로 실행되어 동시에 동작한다.


join()

join() 메서드는 스레드가 종료될 때까지 기다리는 함수이다. 아래 예제를 보자.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name  # 스레드 이름 지정

    def run(self):
        print("sub thread start", threading.currentThread().getName())
        time.sleep(1)
        print("sub thread end", threading.currentThread().getName())

threads = []
for i in range(5):
    thread = Worker(i)
    thread.start()   # 서브 스레드의 start 메서드 호출
    threads.append(thread)

# join() 사용 시 아래 주석을 해제한다.
# for thread in threads:
#     thread.join()

print("메인 종료")

실행 결과를 보면, join()을 사용하지 않으면 스레드가 모두 종료되기 전에 "메인 종료"가 출력된다.
반면, join()을 사용하면 모든 스레드가 종료된 후에 "메인 종료"가 출력된다.
이처럼 join()은 특정 스레드가 종료된 후 이후 작업을 진행하고자 할 때 유용하게 사용할 수 있다.


is_alive()

마지막으로 is_alive() 메서드는 해당 스레드가 실행 중인지(즉, 살아있는지) 여부를 불리언 값으로 알려준다.

import threading
import time

class Worker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name  # 스레드 이름 지정

    def run(self):
        time.sleep(1)

thread = Worker("comgong")
thread.start()         

print(thread.is_alive())
thread.join()
print(thread.is_alive())

실행 결과는, 스레드를 시작한 후에는 True를 반환하지만, join()을 호출하여 스레드가 종료된 후에는 False를 반환한다.
이렇게 is_alive()를 통해 스레드의 현재 상태를 확인할 수 있다.


마무리

여기서 한 가지 질문을 해본다.
일반 실행 시 5초가 걸리는 프로그램이 있다면, 이를 5개의 서브 스레드를 사용해 실행하면 몇 초가 걸릴까?
1초? 5초?
다음 글에서는 파이썬의 GIL(Global Interpreter Lock) 정책에 대해 설명할 예정이다.

이상으로 스레드 관련 주요 메서드에 대해 설명했다.

위로 스크롤