[파이썬] 예제로 알아보는 싱글 쓰레드(Single Thread)와 멀티 쓰레드(multi Thread) 개념 이해

파이썬 언어는 인터프리터 언어입니다.

파이썬은 싱글 쓰레드(Single Thread)에서 순차적으로 동작하며, 한 번에 하나의 작업 밖에 할 수 없어요.

한 번에 여러 작업(병렬처리)을 하기 위해서는 threading 모듈을 사용하여 쓰레드를 구현해야 합니다. 

파이썬은 전역 인터프리터 락킹(Global Interpreter Lock) 때문에 특정 시점에 하나의 파이썬 코드만을 실행하게 되어 있습니다.

그럼으로 파이썬은 실제 다중 CPU 환경에서 동시에 여러 파이썬 코드를 병렬로 실행할 수 없어요.

그래서 인터리빙(Interleaving) 방식으로 코드를 분할하여 실행됩니다.

다중 CPU 에서 병렬 실행을 위해서는 다중 프로세스를 이용하는 multiprocessing 모듈을 사용해야 합니다. 

파이썬에서 병렬처리를 구현하는 방법

멀티 쓰레드를 또는 멀티프로세스를 사용합니다.

1. 싱글 쓰레드 구현하기: threading 모듈 사용

다음 코드 스니펫은 0부터 1억까지의 합을 구하는 계산 프로그램을 1개의 쓰레드로 동작하도록 만들어서 실행합니다. 

start()함수로 쓰레드를 시작합니다.

 join()함수는 쓰레드가 모든 작업을 마칠 때까지 기다리는 것을 의미합니다. 

Thread가 인자로 받는 target은 쓰레드가 실행할 함수를 대입합니다. 그리고 args는 쓰레드가 실행하는 함수의 인자들을 의미합니다.

from threading import Thread
import time


def addition_operation(work_id, start, end, result):
    print(f'작업단위 : {work_id}')
    total = 0
    for i in range(start, end):
        total += i
    result.append(total)
    return


result_list = list()
thread1 = Thread(target=addition_operation, args=(1, 0, 100000000, result_list))
thread1.start()
start_time = time.perf_counter() #스톱워치랑 같은 개념
start_time5 = time.process_time() #코드 효율성비교는 process_time()함수를 사용하는 것이 유용

thread1.join()
print(f"덧셈결과: {sum(result_list)}")
end_time = time.perf_counter() 
end_time5 = time.process_time()

print("스톱워치 : 쓰레드 작업 실행시간(초): ", end_time - start_time)
print("코드 효율성비교 : 프로세스 실행시간: ", end_time5 - start_time5)



#실행결과
작업단위 : 1
덧셈결과: 4999999950000000
스톱워치 : 쓰레드 작업 실행시간(초):  5.6459868
코드 효율성비교 : 프로세스 실행시간:  5.53125

2. 멀티쓰레드 구현하기 : threading 모듈 사용

다음 코드 스니펫 예제는 쓰레드를 하나 더 추가하여 병렬로 동작하도록 멀티 쓰레드를 구현하여 실행하여봅니다.

1억까지의 합을 구하기 위해 2개의 쓰레드로 각각 5천만까지 합을 구합니다.

from threading import Thread
import time


def addition_operation(work_id, start, end, result):
    print(f'작업단위 : {work_id}')
    total = 0
    for i in range(start, end):
        total += i
    result.append(total)
    return


result_list = list()
thread1 = Thread(target=addition_operation, args=(1, 0, 50000000, result_list))
thread2 = Thread(target=addition_operation, args=(2, 50000000, 100000000, result_list))
thread1.start()
start_time = time.perf_counter() #스톱워치랑 같은 개념
start_time5 = time.process_time() #코드 효율성비교는 process_time()함수를 사용하는 것이 유용
thread2.start()
thread1.join()
thread2.join()
print(f"덧셈결과: {sum(result_list)}")

end_time = time.perf_counter()
end_time5 = time.process_time()

print("스톱워치 : 쓰레드 작업 실행시간(초):", end_time - start_time)
print("코드 효율성비교 : 프로세스 실행시간:", end_time5 - start_time5)


#실행결과
작업단위 : 1
작업단위 : 2
덧셈결과: 4999999950000000
스톱워치 : 쓰레드 작업 실행시간(초): 5.3284284
코드 효율성비교 : 프로세스 실행시간: 5.3125

싱글 쓰레드로 처리한 실행시간과 병렬처리(멀티 쓰레드)로 처리한 실행시간의 차이가 거의 없습니다.

그 이유는 파이썬의 전역 인터프리터 락킹(Global Interpreter Lock) 정책(GIL) 때문입니다. GIL정책이 적용되는 것은 CPU동작에서 입니다. 

하나의 쓰레드가 cpu 작업을 마치고 I/O 작업을 실행하는 동안에는 다른 쓰레드가 cpu 동작을 동시에 실행할 수 있습니다. 

그럼으로 cpu작업이 적고 I/O 작업이 많은 병렬처리 프로그램에서 효과를 볼 수 있어요.

반면 다중 프로세스를 이용하는 multiprocessing 모듈를 사용하여 구현하는 경우 멀티프로세스 각자가 고유한 메모리를 할당 받아서 사용하기 때문에 많은 메모리를 필요로 합니다. 그러나 작업을 병렬로 동시에 처리합니다. 분산 처리 시스템을 구현할 수 있겠죠?

파이썬 멀티프로세싱(multiprocessing) 구현하는 방법

파이썬에서 다중 CPU에서 병렬 실행을 위해서는 다중 프로세스를 이용하는 multiprocessing 모듈을 사용해야 합니다. 

multiprocessing 모듈은 쓰레드(Thread) 대신 프로세스를 만들어 병렬로 처리합니다.  구현 방법은 Thread를 구현하던 방법과 거의 동일합니다.

Thread함수 대신 Process함수를 사용하여 처리하고, result 결과값을 받기 위해 Queue를 사용합니다.

Queue는 대표적인 데이터 구조의 하나로 FIFO(First-In, First-Out) 방식입니다.

가장 먼저 큐에 들어온 놈이 가장 먼저 나간다는 의미입니다.

한 예로, 은행에서 대기표 받고 기다리는 것을 생각하면 이해하기 쉽습니다. 먼 저 대기표 뽑은 사람이 먼저 나가죠!

from multiprocessing import Process, Queue
import time


def addition_operation(work_id, start, end, result):
    print(f'작업단위 : {work_id}')
    total = 0
    for i in range(start, end):
        total += i
    result.put(total)
    return

if __name__ == '__main__':
    result_list = Queue()
    process1 = Process(target=addition_operation, args=(1, 0, 50000000, result_list))
    process2 = Process(target=addition_operation, args=(2, 50000000, 100000000, result_list))
    start_time = time.perf_counter() #스톱워치랑 같은 개념
    start_time5 = time.process_time() #코드 효율성비교는 process_time()함수를 사용하는 것이 유용
    process1.start()
    process2.start()
    process1.join()
    process2.join()


    total = 0
    qsize = result_list.qsize()
    while qsize > 0:
        total = total + result_list.get()
        qsize = qsize - 1
        

    print(f"덧셈결과: {total}")
    end_time = time.perf_counter()
    end_time5 = time.process_time()

    print("스톱워치 : 쓰레드 작업 실행시간(초):", end_time - start_time)
    print("코드 효율성비교 : 프로세스 실행시간:", end_time5 - start_time5)
    
    
#실행결과    
작업단위 : 1
작업단위 : 2
덧셈결과: 4999999950000000
스톱워치 : 쓰레드 작업 실행시간(초): 3.6920032
코드 효율성비교 : 프로세스 실행시간: 0.015625    

실행결과를 프로세스 실행시간이 0.015625초 입니다.

멀티 쓰레드로 구현하여 처리하였을 때는 5.3125초가 나왔습니다.

각각의 프로세스가 각자의 메모리 영역을 사용하게됨으로 처리속도가 훨씬 빨라졌어요.

쓰레드와의 또 다른 차이점은 멀티프로세스를 구현할때

if __name__ == ‘__main__’: 조건문을 무조건 사용해야합니다. 그렇지 않으면 실행할때 오류가 발생하였습니다.

쓰레드는 어려운 것 같으면서도 한 번 이해하면 쉽습니다.

오늘도 수고했어요.

카테고리 글 더 보기

error: Content is protected !!