[Python] 멀티 스레드 - 1

AirPlaneMode·2022년 1월 23일
1

파이썬

목록 보기
1/4

서론

멀티 스레드 (Multi-thread)는 보통 언어 기본서 맨 마지막에 나오는 부분으로, 면접에서 어떤 언어에 대한 숙련도를 확인할 때 나오는 단골 질문이라고 한다.

개인적으로는 멀티 스레딩의 기본적인 개념을 알고 있었지만, 알고리즘 만으로 파이썬을 익혔기 때문에 파이썬으로 구현해본 적은 없어 면접 질문에 제대로 대답하지 못했던 경험이 있다.

본 문서에서는 파이썬으로 멀티 스레드를 한 번 구현해보는 것으로 멀티 스레딩에 대한 기본적인 이해도를 갖추는 것을 목표로 한다.

본론

스레드란?

위키피디아의 스레드 문서에서는 스레드를 다음과 같이 정의한다.

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system

운영체제의 일부인 스케쥴러에 의해 독립적으로 관리되는 가장 작은 명령(instruction)의 연속(sequence)을 스레드라고 한다.

멀티 스레딩이란?

멀티 스레딩은 스레드를 병렬적으로 동시에(concurrently) 실행하는 것을 의미한다.

멀티 프로세스와의 차이는?

멀티 스레드는 하나의 CPU 내의 자원을 공유(share)하며 사용하며, 멀티 프로세스는 각각의 프로세스가 각자의 자원을 사용한다는 차이가 존재한다.

파이썬에서의 스레드

순차적 실행

import time

def thread_first():
    print("the first thread starts")
    for i in range(5):
        print(f"thread_1 is running {i}")
        time.sleep(0.1)
    print("the first thread ends")

def thread_second():
    print("the second thread starts")
    for i in range(5):
        print(f"thread_2 is running {i}")
        time.sleep(0.1)
    print("the second thread ends")

def thread_third():
    print("the third thread starts")
    for i in range(5):
        print(f"thread_3 is running {i}")
        time.sleep(0.1)
    print("the third thread ends")

class Timer():
    def __init__(self):
        self.start_time = time.time()
        self.end_time = None

    def end(self):
        self.end_time = time.time()
        print(f"time spent {self.end_time - self.start_time}")

timer = Timer()

thread_first()
thread_second()
thread_third()

timer.end()

우선 각각의 메소드를 정의해준다. 각 메소드는 처음 호출을 당할 때 함수가 시작됨을 알리고, 0.5초 간격으로 1부터 5까지의 숫자를 출력한 후, 함수가 끝남을 알린다.

그리고 세 개의 함수가 모두 호출되고 종료될 때까지의 시간을 측정한다.

the first thread starts
thread_1 is running 0
thread_1 is running 1
thread_1 is running 2
thread_1 is running 3
thread_1 is running 4
the first thread ends
the second thread starts
thread_2 is running 0
thread_2 is running 1
thread_2 is running 2
thread_2 is running 3
thread_2 is running 4
the second thread ends
the third thread starts
thread_3 is running 0
thread_3 is running 1
thread_3 is running 2
thread_3 is running 3
thread_3 is running 4
the third thread ends
time spent 1.64677095413208

첫 번째 함수 thread_1이 호출되고 작업이 다 진행되어 종료된 후에 다음 함수인 thread_2, thread_3이 순차적으로 호출됨을 알 수 있다.

멀티 스레드 실행

파이썬에서 멀티 스레드를 사용하기 위해서는 threading 라이브러리를 사용한다.

thread_1 = threading.Thread(target = thread_first)
thread_2 = threading.Thread(target = thread_second)
thread_3 = threading.Thread(target = thread_third)

timer = Timer()

thread_1.start()
thread_2.start()
thread_3.start()

timer.end()

앞서 함수 및 클래스 정의는 동일하며, 이후 부분을 이전과 같이 수정하였다. 각 Thread는 start 함수를 호출하여 thread_nth 함수를 실행시킨다.

the first thread starts
thread_1 is running 0
the second thread starts
thread_2 is running 0
the third thread starts
time spent 0.0015048980712890625
thread_3 is running 0
thread_3 is running 1
thread_2 is running 1
thread_1 is running 1
thread_3 is running 2
thread_2 is running 2
thread_1 is running 2
thread_3 is running 3
thread_2 is running 3
thread_1 is running 3
thread_1 is running 4
thread_2 is running 4
thread_3 is running 4
the third thread ends
the first thread ends
the second thread ends

보이는 것과 같이 어느 하나의 스레드가 끝나기도 전에 timer.end()가 호출되는 것을 확인할 수 있다. 그 이후에는 각각의 스레드가 병렬적으로 실행되는 것을 확인할 수 있다.

뭐지?

이에 하나의 궁금증이 생겼다. 세 개의 스레드가 다 끝나기도 전에 timer.end()가 호출되었다면, 그 위치에 만약 반복문이 있다면 어떻게 될까? 하는 궁금증이었다. 이에 반복문을 하나 추가하였다.

timer = Timer()

thread_1.start()
thread_2.start()
thread_3.start()

for i in range(10):
    print(f"here is not thread {i}")
    time.sleep(0.1)

timer.end()
the first thread starts
thread_1 is running 0
the second thread starts
thread_2 is running 0
here is not thread 0
the third thread starts
thread_3 is running 0
here is not thread 1
thread_2 is running 1
thread_3 is running 1
thread_1 is running 1
thread_3 is running 2
thread_2 is running 2
here is not thread 2
thread_1 is running 2
thread_2 is running 3
thread_3 is running 3
here is not thread 3
thread_1 is running 3
thread_2 is running 4
thread_1 is running 4
thread_3 is running 4
here is not thread 4
the first thread ends
here is not thread 5
the third thread ends
the second thread ends
here is not thread 6
here is not thread 7
here is not thread 8
here is not thread 9
time spent 1.0891258716583252

세 개의 스레드가 병렬적으로 실행되다가, for-loop를 만나면 세 스레드는 활동을 멈추고 스레드가 아닌 for-loop가 10번 반복된 이후 멈췄던 세 개의 스레드가 다시 활동을 시작할 것으로 예측하였다.

그러나 결과를 보면 세 개의 스레드와 for-loop가 동시에 실행되는 것처럼 보인다.

정확한 이유는 찾아봐야 알겠지만, main 함수는 굳이 스레드로 만들지 않더라도 스레드로 취급하는 것이 아닐까?라고 생각하였다.

수정 (2022.01.23)
threading.current_thread()를 찍어보면 MainThread가 실행되고 있음을 확인할 수 있다. 따라서 나의 가정이 정답에 가깝다는 것을 확인할 수 있었다.

Join 함수

그렇다면 세 개의 스레드가 다 반복된 이후에 걸린 시간을 측정하고 싶을 땐 어떻게 해야 할까? 이는 스레드의 join함수를 이용한다.

join함수는 thread가 종료될 때까지 기다리는 역할을 수행한다.


timer = Timer()

thread_1.start()
thread_2.start()
thread_3.start()

thread_1.join()
thread_2.join()
thread_3.join()

timer.end()

따라서 코드를 다음과 같이 수정한 후에 결과를 출력하였다.

the first thread starts
thread_1 is running 0
the second thread starts
thread_2 is running 0
the third thread starts
thread_3 is running 0
thread_3 is running 1
thread_2 is running 1
thread_1 is running 1
thread_2 is running 2
thread_1 is running 2
thread_3 is running 2
thread_3 is running 3
thread_2 is running 3
thread_1 is running 3
thread_1 is running 4
thread_2 is running 4
thread_3 is running 4
the second thread ends
the third thread ends
the first thread ends
time spent 0.5480244159698486

보는 것과 같이 세 개의 스레드가 모두 종료된 이후에야 총 소요 시간이 출력된 것을 확인할 수 있다. join 함수는 위치에 따라 결과가 다르게 출력된다.

만약 아래와 같이 startjoin을 같이 실행한다면, 순차적 실행과 같은 결과가 나올 것이다.

timer = Timer()

thread_1.start()
thread_1.join()

thread_2.start()
thread_2.join()

thread_3.start()
thread_3.join()

timer.end()
the first thread starts
thread_1 is running 0
thread_1 is running 1
thread_1 is running 2
thread_1 is running 3
thread_1 is running 4
the first thread ends
the second thread starts
thread_2 is running 0
thread_2 is running 1
thread_2 is running 2
thread_2 is running 3
thread_2 is running 4
the second thread ends
the third thread starts
thread_3 is running 0
thread_3 is running 1
thread_3 is running 2
thread_3 is running 3
thread_3 is running 4
the third thread ends
time spent 1.6537072658538818

thread_1이 시작되고 jointhread_1이 끝날 때까지 대기하였다가 끝나고 나서야 thread_2를 시작하기 때문이다.

join 함수에는 시간을 변수로 넣을 수 있다. 가령 thread_1.join(0.3)과 같이 코딩한다면 0.3초간은 thread_1 홀로 실행되다가 0.3초부터는 thread_1thread_2가 동시에 실행될 것이다.

0개의 댓글