파이썬 예제 (multiprocessing)

개요

안녕하세요.

오늘은 파이썬의 multiprocessing (프로세스 기반 병렬처리) 에 대해 살펴보려고 합니다.

아시다시피 파이썬은 GIL (Global Interperter Lock) 메커니즘으로 구현되어 있으며, 이로인해 얻은 이점도 (Thread-Safe) 있지만, Multi-Thread 기반 병렬처리에서 많은 손해를 봅니다.

 

잠깐 상상해 볼까요.

만약 1부터 1억까지 모든 수가 나열된 수열이 있고, 이를 모두 더한 합을 구하고 싶습니다.

싱글쓰레드, 멀티쓰레드, 멀티프로세싱 모두 답은 얻을 수 있지만 무엇이 가장 빠를까요?

자세한 내용은 뒤에 소개하겠습니다.

 

우선 이해를 돕기위해 Application, Proecss, Thread 개념을 간단히 살펴보고 진행하겠습니다.


Application (앱)

줄여서 앱(App)이라고 부르며, 어떤 목적을 가지고 실행중인 프로그램입니다.

하나 이상의 Proecss로 구성됩니다.

아래 그림 예시에서 게임앱은 2개의 프로세스가 서로 통신 (IPC, 프로세스간 통신) 하며 실행되고 있고 엑셀앱은 단일 프로세스로 실행중인 모습입니다.

앱은 1개이상의 프로세스로 구성될 수 있습니다.

그리고 프로세스는 1개의 메인쓰레드를 (실행흐름) 갖지만, 별도로 N개의 쓰레드를 생성해 구성할 수도 있습니다.

[App, Process, Thread 관계]

Process (프로세스)

실행중인 앱을 구성하는 단위(Task)이며, 1개 이상의 프로세스가 모여 앱을 구성합니다.

하나의 앱은 꼭 1개의 프로세스가 아니라 다수의 프로세스로 구성될 수도 있다는 의미입니다.

예를 들어 게임이라면 아군이동, 적군이동, 코드가 프로세스별로 따로 구성될 수 있습니다.

프로세스는 운영체제로부터 다른 프로세스들과 독립된 메모리 영역 (Code, Data, Stack, Heap) 을 보장받습니다.

따라서 동일한 앱내에서도, 프로세스가 다르면 서로 변수, 함수, 등의 자원이 공유되지 않으며 이를 해결하기 위해 Inter-Process Communication (프로세스간 통신) 가 필요합니다.

 

[실행중인 Process 목록]

 

Thread (쓰레드)

Process내부 실행흐름의 단위를 말합니다.

프로세스는 최소 1개의 실행흐름을 가지며, 이를 메인쓰레드라고 부릅니다.

필요한 경우 별도의 실행흐름(쓰레드) 을 추가해, 프로세스 내에서 새로운 코드실행의 흐름을 만들수 있습니다.

일반적으로 프로세스내 어떤 작업이 많은 연산 or 지속적인 반복을 요하는 경우 전체 코드가 지연되게 되는데 이때, 별도의 실행흐름을 만들어 여러 행동을 동시에 수행할 목적으로 주로 사용됩니다.

이때, 쓰레드간 공유자원에 대한 Race-Condition에 주의해야 합니다.

쓰레드는 생성시 독립적인 메모리 Stack을 따로 가지며 나머지는 프로세스와 공유합니다.

[프로세스의 Thread 수]

 

이제 1~100,000,000 까지 모든 수를 더한 합을 구하는 예제를 Single-Thread, Multi-Thread, Multi-Processing 3가지 방법으로 실험해 보겠습니다.

아래에 소개할 예제는 Summary() 라는 함수에 시작, 끝 값을 입력하면 모든 수를 더해 결과를 돌려주는 역할을 수행합니다.

예를 들면 summary(1, 10) 을 실행하면 답은 1~10의 합인 55가 출력됩니다.


Case 1: Single Thread

메인쓰레드에 코드를 작성한 결과입니다.

import time

def summary(a, b):
    sum = 0
    for i in range(a, b+1):
        sum+=i

    return sum

if __name__ == '__main__':
    s = time.time()
    print( summary(1,100000000) )
    e = time.time()

    print(f'Elapsed time : {e-s:.3f} sec')

결과는

5000000050000000
Elapsed time : 5.397 sec


Case 2: Multi Thread

별도의 실행흐름,Thread를 2개 생성하고, 합을 구할 숫자는 반으로 나누어 각각 실행합니다.

from threading import Thread
from multiprocessing import Queue
import time

def summary(a, b, q):
    sum = 0
    for i in range(a, b+1):
        sum+=i
    q.put(sum)

if __name__ == '__main__':
    s = time.time()
    q1 = Queue()
    q2 = Queue()
    t1 = Thread(target=summary,  args=(1, 50000000, q1))
    t1.start()
    t2 = Thread(target=summary, args=(50000001,100000000, q2))
    t2.start()

    t1.join()
    t2.join()
    print( sum([q1.get(), q2.get()]) )
    e = time.time()

    print(f'Elapsed time : {e - s:.3f} sec')

결과는

5000000050000000
Elapsed time : 6.277 sec

Single Thread보다 더 오래 걸린 이유는 Python의 GIL 때문에 쓰레드가 동시에 실행될 수 없으므로 성능상의 이득을 기대하기 어렵고, 각 쓰레드의 전환 (Context Switching)에 오히려 많은 자원이 소비된 것으로 추측됩니다.


Case 3: Multi Processing

Process Class로 프로세스를 2개 생성하고, 합을 구할 숫자를 반씩 나누어 각 프로세스가 수행됩니다. 

파이썬은 프로세스간 통신을 위해 Queue, Pipe Class를 제공하는데 예제에서는 Queue 클래스를 사용하였습니다.

from multiprocessing import Process, Queue
import os
import time

def summary(a, b, q):
    sum = 0
    for i in range(a, b+1):
        sum+=i
    q.put(sum)

if __name__ == '__main__':
    s = time.time()
    q1 =  Queue()
    q2 = Queue()
    p1 = Process(target=summary,  args=(1, 50000000, q1))
    p1.start()
    p2 = Process(target=summary, args=(50000001, 100000000, q2))
    p2.start()

    p1.join()
    p2.join()
    print(sum([q1.get(), q2.get()]))
    e = time.time()

    print(f'Elapsed time : {e - s:.3f} sec')

결과는

5000000050000000
Elapsed time : 3.484 sec

앞선 2가지 테스트 대비 성능향상이 존재함을 확인할 수 있습니다.

그 이유는 Multi-Processing을 이용해 별도의 Sub Process를 생성해 병렬처리되었기 때문입니다. 

따라서 Python GIL을 효과적으로 우회할 수 있었고 결과적으로 성능 향상이 있었습니다.

다만, 개별 프로세스는 별도의 메모리 공간을 가지므로 IPC, 동기화에 대한 처리에 주의해야 하며, 당연하게도 앱의 CPU 점유율 또한 상승하게 됩니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)