Technology issues/Tech_Trend

파이썬 멀티프로세싱 - 멀티스레딩과 GIL의 한계 그리고 대안은 없을까 #2

pauls 2024. 12. 31. 12:19
반응형

오늘은 "파이썬 멀티스레딩과 GIL의 한계 그리고 대안"과 관련한 두 번째 이야기로 멀티프로세싱에 대해 정리해 보려 합니다. 지난 글에서도 GIL의 한계로 파이썬에도 멀티스레드가 불가하면 그 대안으로 멀티프로세싱과 비동기 프로그래밍을 언급한 바 있습니다. 오늘은 이제 두 가지 대안 중 하나인 멀티프로세싱에 대해 알아보겠습니다.

파이썬 멀티프로세싱
파이썬 멀티프로세싱

 

 

파이썬 멀티프로세싱

멀티프로세싱은 간단히 말해 여러 프로세스를 동시에 실행하여 병렬 처리를 수행하는 방식입니다.

  • 멀티스레딩이 ‘하나의 프로세스 안에서 여러 실행 흐름(스레드)을 두는 것’이라면
  • 멀티프로세싱은 ‘프로세스 자체를 여러 개 띄우는 것’이라고 이해할 수 있습니다.

프로세스마다 독립된 메모리 공간을 가지기 때문에, 하나의 프로세스가 다른 프로세스의 메모리에 직접 접근하기는 어렵지만, 대신 GIL 문제에서 비교적 자유롭다는 장점이 있습니다.

 

 

반응형

 

  멀티스레딩과의 차이점

  • 메모리 공유: 멀티스레딩에서는 동일한 주소 공간을 공유하므로, 여러 스레드가 동시에 접근하는 자원 관리가 중요합니다. 반면, 멀티프로세싱에서는 프로세스 간에 기본적으로 메모리를 공유하지 않습니다.
  • IPC(프로세스 간 통신): 멀티프로세싱에서는 각 프로세스가 독립적으로 움직이다 보니, 데이터를 주고받기 위해서는 별도의 IPC(Inter-Process Communication) 기법이 필요합니다.
  • GIL의 영향: 파이썬에서 스레드는 GIL을 피할 수 없지만, 프로세스는 각자 해석기(인터프리터)를 따로 두기에 GIL의 영향을 받지 않습니다.

 

  멀티프로세싱 라이브러리

파이썬은 표준 라이브러리에 `multiprocessing` 모듈을 포함하고 있어서, 추가 설치 없이 손쉽게 멀티프로세싱을 구현할 수 있습니다. 이 모듈은 스레드 대신 프로세스를 생성하여 작업을 병렬로 처리하므로, GIL이 존재한다 해도 각 프로세스가 독립적으로 코드를 실행할 수 있습니다.

 

 

 

 

 

주요 클래스와 함수

  • Process 클래스: 가장 기본적인 멀티프로세싱 단위를 구성합니다. 생성자에 타깃 함수와 인자를 넘겨주면, 독립적인 프로세스로 해당 함수를 실행합니다.
  • Pool 클래스: 병렬 작업을 처리하기 위해 정해진 개수의 프로세스 풀을 만들어 관리합니다. 주로 맵(map) 함수 등을 활용하여 여러 입력값에 대한 함수를 병렬로 실행합니다.
  • Queue, Pipe: 프로세스 간 통신(IPC)에 사용하는 객체들입니다. 데이터를 프로세스 간에 주고받을 때 유용합니다.

 

예시 코드 1.

다음은 multiprocessing 모듈에서 Process 클래스를 이용해 프로세스를 두 개 생성하고 실행하는 예시입니다.

from multiprocessing import Process import os import time def worker(name): print(f"[{os.getpid()}] {name} 작업을 시작합니다.") time.sleep(2) print(f"[{os.getpid()}] {name} 작업이 끝났습니다.") if __name__ == "__main__": # 프로세스 두 개 생성 p1 = Process(target=worker, args=("작업1",)) p2 = Process(target=worker, args=("작업2",)) p1.start() p2.start() p1.join() p2.join() print("메인 프로세스가 종료됩니다.")
  •  `Process(target=worker, args=("작업1",))`는 `worker("작업1")` 함수를 별도의 프로세스에서 실행하겠다는 의미입니다.
  •  `start()` 메서드를 호출하면 프로세스가 실제로 시작되어 병렬로 동작합니다.
  •  `join()`을 호출하면 해당 프로세스가 종료될 때까지 메인 프로세스가 기다립니다.

이처럼 각 프로세스는 GIL에 구애받지 않고 독립적으로 동작하기 때문에, CPU를 효율적으로 나눠 쓸 수 있게 됩니다.

 

 

 

 

 

예시 코드 2.

다음 예시는 가상의 숫자 리스트를 받아 각각의 항목을 제곱한 뒤, 그 결과를 합산하는 작업을 병렬로 처리 코드입니다.

import multiprocessing def square_and_sum(numbers): return sum(x*x for x in numbers) if __name__ == "__main__": # 예시 데이터 data = list(range(1_000_000)) # 병렬 처리할 데이터를 적절히 분할 chunk_size = len(data) // 4 # 4개 프로세스로 나눈다고 가정 chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)] with multiprocessing.Pool(processes=4) as pool: results = pool.map(square_and_sum, chunks) print("결과 합계:", sum(results))
  • 여기서 Pool 객체를 이용해 4개의 프로세스를 생성하고, 데이터 리스트를 4등분하여 각 프로세스가 분산 처리하도록 합니다.
  • 병렬화로 인해 CPU 코어를 최대한 활용할 수 있으므로, 한 프로세스만 사용할 때보다 훨씬 빠르게 결과를 얻을 수 있습니다.

 

 

 

 

예시 코드 3.

이번에는 단일 프로세스와 멀티프로세스의 처리 시간을 비교해 보도록 하겠습니다. 간단한 CPU 계산 방식은 단일 프로세스가 비교적 빠른 경우가 많습니다. 하지만 복잡한 경우는 다르죠.

아래 예시는 CPU 집약적인 작업을 하도록 하기 위해 "Leibniz" 공식을 사용하여 π(파이)의 근사값을 계산하는 코드입니다.

from multiprocessing import Pool import time import math def calculate_pi(n): """ Leibniz formula for π π = 4 * (1 - 1/3 + 1/5 - 1/7 + ...) """ result = 0 for i in range(n): result += math.pow(-1, i) / (2 * i + 1) return 4 * result if __name__ == '__main__': iterations = 100000000 num_processes = 8 # 단일 프로세스 실행 start = time.time() result = calculate_pi(iterations) end = time.time() print(f"단일 프로세스 실행 시간: {end - start:.2f}초") print(f"단일 프로세스 결과: {result}") # 멀티프로세싱 실행 start = time.time() with Pool(num_processes) as p: chunk_size = iterations // num_processes results = p.map(calculate_pi, [chunk_size] * num_processes) result = sum(results) / num_processes end = time.time() print(f"멀티프로세싱 실행 시간: {end - start:.2f}초") print(f"멀티프로세싱 결과: {result}")

이 코드의 실행 결과는 다음과 같이 출력됩니다.

멀티프로세싱_출력결과
멀티프로세싱_출력결과

 

멀티프로세싱이 단일 프로세스보다 4배 정도 빠르게 작업을 수행했습니다.

 

 

 

 

 

  멀티프로세싱의 장단점

  • 장점
    • CPU 다중 코어 활용: 여러 프로세스를 실행하므로, CPU 코어를 온전히 활용할 수 있어 CPU 집약적 작업(CPU-bound 작업)의 성능이 향상됩니다.
    • 안정성: 프로세스가 각각 독립된 메모리를 사용하므로, 한 프로세스에서 발생한 문제(메모리 파손 등)가 다른 프로세스에 직접 전이되지 않습니다.
    • 확장성: 주어진 작업을 여러 프로세스에 균등하게 배분하는 구조를 만들면, 코어 수에 비례해 확실한 속도 향상을 기대할 수 있습니다.
  • 단점
    • 프로세스 간 통신 비용: 프로세스는 메모리를 공유하지 않으므로, 데이터를 주고받기 위해서는 직렬화(예: 피클링)와 같은 추가 오버헤드가 발생합니다.
    • 메모리 사용량 증가: 프로세스마다 독립된 메모리 공간을 차지하므로, 동시 실행 프로세스 수에 따라 메모리를 많이 사용하게 됩니다.
    • 관리의 복잡도: 프로세스 생성과 종료, 통신, 예외 처리 등을 체계적으로 관리하려면 코드가 복잡해질 수 있습니다.

 

 

 

 

 

  멀티프로세싱 활용 사례

  1. 대규모 데이터 처리: 큰 파일을 여러 조각으로 나누어 병렬로 처리하거나, 복수의 데이터셋을 동시에 가공하는 작업은 멀티프로세싱을 통해 상당한 성능 향상을 기대할 수 있습니다.
  2. CPU 연산이 많은 작업 분산:  복잡한 수학 연산, 이미지/영상 처리, 머신 러닝 모델 학습 등은 CPU를 지속적으로 활용하기 때문에 멀티프로세싱의 진가를 발휘할 수 있는 대표적인 사례입니다.
  3. 파이프라인 구성 시의 프로세스 분리:  분석 파이프라인이나 대규모 ETL(Extract-Transform-Load) 작업에서 단계별로 프로세스를 띄워 각 단계의 로직을 격리하면, 한 단계에서 오류가 발생해도 다른 단계에 미치는 영향을 최소화할 수 있습니다.

 

 

멀티프로세싱은 파이썬의 GIL 문제를 우회하는 대표적인 대안으로, CPU 집약적 작업을 여러 코어에 고루 분산하여 병렬 처리할 수 있게 해 줍니다. 다만 프로세스 간 통신(IPC) 방식이나 메모리 사용량, 자원 동기화 기법 등에 대해 주의를 기울여야 효율적인 병렬 처리 구조를 설계할 수 있습니다. 

반응형