파이썬 멀티프로세싱 Pool과 Process 차이점 비교



작성시간 :





(수정됨)

파이썬 멀티프로세싱에 대해 인터넷을 검색해보면 이런 말이 자주 보였다.

Pool은 처리할 일을 바닥에 뿌려놓고 알아서 분산 처리를 하게 만드는 방식이고,
Process는 각 프로세스별로 할당량을 명시적으로 정해준 뒤 실행되는 방식이다.

(그래서 이게 무슨 뜻이라는 거야..)

개인적으로 저 설명을 통해 무엇을 할 수 있는 것인지 알 수가 없다고 생각한다.
심지어 저 말을 적어둔 모든 사람들의 글을 보다보면 저 문구만 적어두고, 정작 저 문구가 무슨 뜻인지 알려주는 사람이 한 명도 없었다.
그들끼리만 통하는 말이라고 생각하고 있다.

그러다 문득, 떠오른 생각이 있어 따로 정리해두기로 했다.

Pool과 Process 비교해보기

사전 설명

먼저, 내가 하려는 것은 단순한 작업 시간 비교다.
각 작업마다 일정 시간 슬립하고, 전체 작업 시간을 출력해서 Process와 Pool이 각각 어떤 차이를 만들 수 있는지를 알아볼 것이다.

멀티프로세싱 코드와 작업시간은 다음과 같다.

multiprocessing.Pool

코드

import multiprocessing as mp
import time
def f(sec):
    sec = int(sec)
    print(f'{sec}초 대기')
    time.sleep(sec)
    print(f'{sec}초 대기 완료')
if __name__ == '__main__':
# https://white.seolpyo.com/
    t = time.time()
    with mp.Pool(processes = 2) as p:
        p.map(f, ['1', '3', '5', '7'])
    print(f'전체 작업 시간 : {time.time() - t}초')

작업시간

>> 1초 대기
3초 대기
1초 대기 완료
5초 대기
3초 대기 완료
7초 대기
5초 대기 완료
7초 대기 완료
전체 작업 시간 : 10.233225107192993초

multiprocessing.Process

코드

import multiprocessing as mp
import time
def f(sec):
    sec = int(sec)
    print(f'{sec}초 대기')
    time.sleep(sec)
    print(f'{sec}초 대기 완료')
if __name__ == '__main__':
# https://white.seolpyo.com/
    t = time.time()
    p1 = mp.Process(target = f, args = ('1'))
    p2 = mp.Process(target = f, args = ('3'))
    p3 = mp.Process(target = f, args = ('5'))
    p4 = mp.Process(target = f, args = ('7'))
    p1.start()
    p2.start()

    p1.join()
    p2.join()

    p3.start()
    p4.start()

    p3.join()
    p4.join()
    print(f'전체 작업 시간 : {time.time() - t}초')

작업시간

>> 1초 대기
3초 대기
1초 대기 완료
3초 대기 완료
5초 대기
7초 대기
5초 대기 완료
7초 대기 완료
전체 작업 시간 : 10.336219072341919초

작업시간을 최소한으로 줄여보기

위 코드들에서는 2개의 프로세스를 사용하며, 각각 1초, 3초, 5초, 7초의 슬립 시간을 가진다.
여기서 가장 최소 작업시간을 소요하는 방법은 cpu1에서 1초 슬립 후 7초 슬립을, cpu2에서 3초 슬립 후 5초 슬립을 하는 것이다.
그러나 위에 있는 모든 코드들은 10초의 작업시간을 보여주는데, 이는 cpu1에서 1초 슬립 후 5초 슬립을, cpu2에서 3초 슬립 후 7초 슬립을 하고 있기 때문이다.
효율적이지 못한 것이다.

이번에는 Process 코드를 개선하여 10초보다 적은 시간을 소모하도록 코드를 수정해볼 것이다.

코드

import multiprocessing as mp
import time
def f(sec):
    sec = int(sec)
    print(f'{sec}초 대기')
    time.sleep(sec)
    print(f'{sec}초 대기 완료')
if __name__ == '__main__':
# https://white.seolpyo.com/
    t = time.time()
    p1 = mp.Process(target = f, args = ('1'))
    p2 = mp.Process(target = f, args = ('3'))
    p3 = mp.Process(target = f, args = ('5'))
    p4 = mp.Process(target = f, args = ('7'))
    p1.start()
    p2.start()

    p1.join()
    p4.start()

    p2.join()
    p3.start()

    p3.join()
    p4.join() 
    print(f'전체 작업 시간 : {time.time() - t}초')

작업시간

약 8초의 작업시간을 소모했다. 대략 2초 정도를 절약하는데 성공했다.

>> 1초 대기
3초 대기
1초 대기 완료
7초 대기
3초 대기 완료
5초 대기
7초 대기 완료
5초 대기 완료
전체 작업 시간 : 8.34662914276123초

변경 전 코드와의 차이점

코드 자체는 큰 차이가 없으나, 코드를 비교해보면 Process의 작업 순서를 변경한 것을 알 수 있다.
코드 실행 부분을 보면 p1작업(1초슬립) 후에 p3(5초슬립)가 아닌 p4작업(7초슬립)을 했고,
p2작업(3초슬립) 후에 p4(7초슬립)가 아닌 p3(5초슬립) 작업을 실행하도록 했다.

결론

여기까지 확인해본 내용을 정리하자면 다음과 같다.

  • Pool의 특징 : 짧은 코드로 다수의 작업을 multiprocessing할 수 있다.
    Pool의 단점 : 설정한 프로세스 수에 따라 작업시간이 크게 변동할 수 있으며, 작업 순서를 예상하기 어렵다.
  • Process의 특징 : 다수의 multiprocessing 작업의 순서를 파악하기 쉽고, 작업순서를 쉽게 예상하고 변경할 수 있다.
    Process의 단점 : 각 프로세스마다 작업을 하나하나 설정하고, start와 join 명령까지 지정해주어야하는 번거로움이 있다.

Pool과 Process의 큰 차이점은 코드 작성자가 직접 작업 순서를 결정할 수 있다는 것에 있다.
거기에 더해 작업이 어떤 순서로 시작되고 종료되는지 코드만 보고도 알 수 있게 된다.

그렇다고 Pool을 사용할 때 작업 순서를 결정할 수 없다는 것은 아니지만, Process를 이용해 코드를 짠 것처럼 작업순서를 쉽게 파악하긴 어렵다.
Pool의 경우 작업이 끝난 프로세스가 다음 작업을 가져가 작업하게 되는데, 작업별 작업시간을 모르니 a 작업을 끝낸 시점에 b 작업이 남아있을지, c 작업이 남아있을지 알 수 없기 때문이다.
Process는 a 작업 후에 b 작업을 할지, c 작업을 할지 코드에서 확인할 수 있기 때문에 비교적 작업순서를 이해하기 쉽다고 생각한다.

이처럼 Process 사용시 코드만 보고도 작업순서가 어떤 식으로 진행되는지 표현할 수 있는 것은 장점이지만, 그만큼 코드가 길어지고 지저분해보일 수 있다는 단점도 된다.
이런 점에서는 Pool을 사용하는 것이 코드가 깔끔하게 보일 수는 있지만, 작업순서를 파악하는데 애를 먹을 수 있기 때문에 무엇이 더 좋다고 말하기는 어려울 것 같다.

Pool은 처리할 일을 바닥에 뿌려놓고 알아서 분산 처리를 하게 만드는 방식이다?

글 서두에 언급한 것처럼 멀티프로세싱에 대해 찾아보면 "Pool은 처리할 일을 바닥에 뿌려놓고 알아서 분산 처리를 하게 만드는 방식"이라는 문구를 찾아볼 수 있다.
이 문장은 어떻게 보면 틀렸다고 할 수 있는데, Pool에 작업 리스트를 갖다주면 그것을 분산처리하는 것이 아니라, 리스트에 담긴 작업을 순서대로 처리할 뿐이다.

즉, "알아서 분산처리를 하게 만드는 것"이 아니라 "여러 개의 코어로 작업을 순서대로 할 뿐인 것"이다.
이것은 큰 차이가 없어보일 수 있으나 사실 엄청난 차이인데, "알아서 분산처리"를 한다는 말은 컴퓨터가 스스로 작업량을 계산한 다음, 효율적으로 작업순서를 조정한다고 생각할 수도 있기 때문이다.
실상은 그렇지 않다. 컴퓨터는 입력받은 작업을 순서대로 수행할 뿐, 알아서 분산처리를 하지는 못한다.

이것을 이해하기 어렵다면 아래 "참고)multiprocessing.Pool에서 작업순서를 조정하는 방법"을 보면 이해가 갈 것이라고 생각한다.

참고)multiprocessing.Process 작업을 2개씩 진행한 이유

수정한 Process 코드를 보면 p1과 p2 join 후 p3과 p4를 start하거나,
p1 join 후 p4 start, p2 join 후 p3 start를 하고 있다.

p1~p4를 한 번에 start한 다음, 한 번에 join하지 않은 이유는 4개의 작업이 동시에 이루어지기 때문이다.
Pool에서는 2개의 프로세스만 사용했는데, Process에서는 4개의 프로세스를 작동시키면 2개의 프로세스(Pool)와 4개의 프로세스(Process) 작업 속도를 비교하게 되버리기 때문에 정확한 비교라고 할 수 없게 된다.

만약 4개의 프로세스를 한 번에 start한 다음 join하게 되면 다음과 같은 결과가 나오게 된다.

import multiprocessing as mp
import time
def f(sec):
    sec = int(sec)
    print(f'{sec}초 대기')
    time.sleep(sec)
    print(f'{sec}초 대기 완료')
if __name__ == '__main__':
# https://white.seolpyo.com/
    t = time.time()
    p1 = mp.Process(target = f, args = ('1'))
    p2 = mp.Process(target = f, args = ('3'))
    p3 = mp.Process(target = f, args = ('5'))
    p4 = mp.Process(target = f, args = ('7'))
    p1.start()
    p2.start()
    p3.start()
    p4.start()

    p1.join()
    p2.join()
    p3.join()
    p4.join()
    print(f'전체 작업 시간 : {time.time() - t}초')

>> 5초 대기
1초 대기
3초 대기
7초 대기
1초 대기 완료
3초 대기 완료
5초 대기 완료
7초 대기 완료
전체 작업 시간 : 7.2880332469940186초

참고)multiprocessing.Pool에서 작업순서를 조정하는 방법

앞서 말한 것처럼 multiprocessing.Pool 역시 작업 순서 조정이 가능하다.
이전 코드와 달라진 부분은 args 부분으로, 기존에는 1, 3, 5, 7이었던 순서가 1, 3, 7, 5로 변경되었다.

이처럼 Pool을 사용하면 Process보다 코드도 짧아 깔끔해보이긴 하지만, 사용하는 프로세스 수에 따라 작업 순서가 달라지게 된다.
만약 나중에 코드를 수정하는 일이 생긴다면 Process로 직접 순서를 정해둔 것보다 애를 먹을 것 같다.

import multiprocessing as mp
import  time
def f(sec):
    sec = int(sec)
    print(f'{sec}초 대기')
    time.sleep(sec)
    print(f'{sec}초 대기 완료')
if __name__ == '__main__':
# https://white.seolpyo.com/
    t = time.time()
    with mp.Pool(processes = 2) as p:
        p.map(f, ['1', '3', '7', '5'])
    print(f'전체 작업 시간 : {time.time() - t}초')

>> 1초 대기
3초 대기
1초 대기 완료
7초 대기
3초 대기 완료
5초 대기
7초 대기 완료
5초 대기 완료
전체 작업 시간 : 8.256280422210693초



태그



공유

하기






white.seolpyo.com