본문 바로가기

Minding's Programming/FastAPI

[FastAPI] 파이썬 멀티 프로세싱

728x90
반응형

해당 글은 인프런의 ' 파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI)'을 수강하며 정리한 글이다.

 

 

파이썬 멀티 프로세싱

동시성과 병렬성 개념 정리 파이썬 멀티 스레딩 사용해보기를 통해서 파이썬이 왜 병렬적 멀티 스레딩 연산을 하지 못하는 지에 대해 알아보았다. '동시성'의 개념을 이용한 I/O bound 코드에 있어 멀티 스레딩은 가능했지만, 파이썬은 GIL이라는 개념이 있기 때문에 스레드로 병렬 연산을 하지 못했다. (Cpu bound 코드와 같이 하나의 계산을 여러 번 반복하는 일 등)

 

파이썬에서 병렬 연산을 위해서는 프로세스 자체를 복사해 사용하는 '멀티 프로세싱'을 사용할 수 있다. 스레드 연산으로는 구현하기 힘든 병렬 연산을 멀티 프로세싱에서는 할 수 있기 때문에, cpu bound로 인한 코드에 개선 효과를 볼 수 있다.

 

그러나 단점도 존재한다. 프로세스를 여러 개 복제해 사용하기 때문에 메모리를 공유하지 않아 프로세스 간 통신이 필요한데, 이에 따른 비용이 든다.(직렬화, 역직렬화의 개념) 따라서 이 비용을 감수할 만큼의 속도 개선이 필요할 때 멀티 프로세싱을 사용하는 것이 좋다.

 

싱글 프로세싱 예시

# 싱글 프로세싱 예시
import os
import threading
import time
import sys

sys.set_int_max_str_digits(1000000)  # 출력할 수 있는 정수 문자열의 한계치 조정

nums = [50, 63, 32]


def cpu_bound_func(num):  # cpu 연산만 필요한 코드: 동시성 프로그래밍이 굳이 필요없음
    print(f"{os.getpid()} process | {threading.get_ident()} thread")
    numbers = range(1, num)
    total = 1
    for i in numbers:
        for j in numbers:
            for k in numbers:
                total *= i * j * k
    return total


def main():
    results = [cpu_bound_func(num) for num in nums]
    print(results)


if __name__ == "__main__":
    start = time.time()
    main()
    end = time.time()
    print(end - start)  # 33초
    
>>>
13420 process | 13740 thread
13420 process | 13740 thread
13420 process | 13740 thread
...
33.343496322631836

cpu bound 코드를 위해 이전의 웹페이지 호출 코드와 다르게 단순한 곱하기 연산을 하는 코드를 준비했다. cpu 연산만 필요한 코드이기 때문에 동시성 프로그래밍의 의미가 적은 코드다. nums라는 리스트에 있는 숫자를 range하여 계속 곱하는 코드다. 싱글 프로세싱에서는 32초의 시간이 걸렸다.

 

CPU 연산을 멀티 스레딩으로 바꾼다면

# cpu 연산을 멀티 스레딩으로 바꿨을 때

import os
import threading
import time
import sys
from concurrent.futures import ThreadPoolExecutor

sys.set_int_max_str_digits(1000000)  # 출력할 수 있는 정수 문자열의 한계치 조정

nums = [50, 63, 32]


def cpu_bound_func(num):  # cpu 연산만 필요한 코드: 동시성 프로그래밍이 굳이 필요없음
    print(f"{os.getpid()} process | {threading.get_ident()} thread")
    numbers = range(1, num)
    total = 1
    for i in numbers:
        for j in numbers:
            for k in numbers:
                total *= i * j * k
    return total


def main():
    executor = ThreadPoolExecutor(max_workers=10)
    results = list(executor.map(cpu_bound_func, nums))
    print(results)


if __name__ == "__main__":
    start = time.time()
    main()
    end = time.time()
    print(end - start)  # 31초: cpu 단순 연산은 멀티 스레딩을 사용해도 큰 차이가 없음
    
>>>
3836 process | 13128 thread
3836 process | 5580 thread
3836 process | 2288 thread
...
31.549022912979126

이번엔 위 코드를 멀티 스레딩으로 구현해보았다. 시간이 다소 줄기는 했지만 안고 가는 위험 부담에 비해 시간 비용 개선이 크게 되지는 않았다.

 

멀티 프로세싱

# 멀티 프로세싱 예시

import os
import threading
import time
import sys
from concurrent.futures import ProcessPoolExecutor

sys.set_int_max_str_digits(1000000)  # 출력할 수 있는 정수 문자열의 한계치 조정

nums = [50, 63, 32]


def cpu_bound_func(num):  # cpu 연산만 필요한 코드: 동시성 프로그래밍이 굳이 필요없음
    print(f"{os.getpid()} process | {threading.get_ident()} thread")
    numbers = range(1, num)
    total = 1
    for i in numbers:
        for j in numbers:
            for k in numbers:
                total *= i * j * k
    return total


def main():
    executor = ProcessPoolExecutor(max_workers=10)
    results = list(executor.map(cpu_bound_func, nums))
    print(results)


if __name__ == "__main__":
    start = time.time()
    main()
    end = time.time()
    print(end - start)  # 30초 (2초 줄음)
    # 극단적인 상황일수록 차이가 커짐
    
>>>
5192 process | 10628 thread
3568 process | 10956 thread
15504 process | 6620 thread
...
29.04854416847229

이번엔 멀티 프로세싱을 사용해보았다. 결과값을 보면 프로세스의 id가 각각 다른 것을 확인할 수 있다. 시간 비용도 개선됐다. 물론 일반 코드와 코루틴 만큼의 차이는 아니지만, 멀티 스레딩을 사용하는 것보다 cpu bound에서 더 좋은 효과를 나타낸다는 것을 알 수 있었다.

728x90