betterway64 concerrent.futures

김승환·2021년 7월 11일

코딩의 기술

목록 보기
35/36

진정한 병렬성을 살리려면 concurrent.futures를 사용하라.

  • 파이썬에서 c언어를 사용하여 높은 연산 성능을 이끄는데 좋지만 비용이 매우 크다.

  • 파이썬의 concurrent.futures 내장 모듈을 통해 사용할 수 있는 multiprocessing내장모듈이 어려운 문제를 풀기위한 해결책일 수 있다.

  • ThreadpoolExecuter 모듈을 이요하면 CPU코어를 활용할 수 있다.
    - 주 인터프리터와 자식 프로세스는 별도 수행을 한다.

  • 예를 들어 계산이 많이 필요한 작업을 여러 CPU 코어를 활용해 파이썬으로 실행하고 싶다고 하자.
    - 계산이 많이 필요한 알고리즘을 대신해 여기서는 두 수의 최대공약수(GCD)를 구하는 구현을 사용한다.

# mymodule.py
def gcd(pair):
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i
    assert False, '도달할 수 없음'
#기존 순차적 계산
# run_serial.py
import my_module
import time

NUMBERS = [
    (1963309, 2265973), (2030677, 3814172),
    (1551645, 2229620), (2039045, 2020802),
    (1823712, 1924928), (2293129, 1020491),
    (1281238, 2273782), (3823812, 4237281),
    (3812741, 4729139), (1292391, 2123811),
]

def main():
    start = time.time()
    results = list(map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f'총 {delta:.3f} 초 걸림')


if __name__ == '__main__':
    main()

총 1.301 초 걸림

threadPoolExecutor 클래스 이용

# run_threads.py
import my_module
from concurrent.futures import ThreadPoolExecutor
import time

NUMBERS = [
    (1963309, 2265973), (2030677, 3814172),
    (1551645, 2229620), (2039045, 2020802),
    (1823712, 1924928), (2293129, 1020491),
    (1281238, 2273782), (3823812, 4237281),
    (3812741, 4729139), (1292391, 2123811),
]

def main():
    start = time.time()
    pool = ThreadPoolExecutor(max_workers=2)
    results = list(pool.map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f'총 {delta:.3f} 초 걸림')

if __name__ == '__main__':
    main()

총 1.292 초 걸림

  • 나의 컴퓨터 기준 더 빨라 졌지만 큰 차이는 없으며 책에서는 오히려 더 오래 걸렸다.
  • 이는 스레트 풀을 시작하고 통신하는데 시간이 발생하기 때문이다.

ProcessPoolExecutor로 바꾸면 더 빨라진다.

ProcessPoolExecutor 클래스는 프로세스 풀을 사용하여 호출을 비동기적으로 실행하는 Executor 서브 클래스입니다. ProcessPoolExecutor 는 multiprocessing 모듈을 사용합니다. 전역 인터프리터 록 을 피할 수 있도록 하지만, 오직 피클 가능한 객체만 실행되고 반환될 수 있음을 의미합니다.

  • 전역 인터프리터 록
    - 한 번에 오직 하나의 스레드가 파이썬 바이트 코드 를 실행하도록 보장하기 위해 CPython 인터프리터가 사용하는 메커니즘. (dict와 같은 중요한 내장형들을 포함하는) 객체 모델이 묵시적으로 동시 액세스에 대해 안전하도록 만들어서 CPython 구현을 단순하게 만듭니다.
  • multiprocessing
    - multiprocessing 패키지는 지역과 원격 동시성을 모두 제공하며 스레드 대신 서브 프로세스를 사용하여 전역 인터프리터 록 을 효과적으로 피합니다.
    이것 때문에, multiprocessing 모듈은 프로그래머가 주어진 기계에서 다중 프로세서를 최대한 활용할 수 있게 합니다.

process와 Thread 차이

  • 프로세스와 스레드의 근본적인 차이는 프로세스는 운영체제로부터 독립된 시간, 공간 자원을 할당 받아 실행된다는 점이고, 스레드는 한 프로세스 내에서 많은 자원을 공유하면서 병렬적으로(Concurrently) 실행된다는 것이다. 다른 차이는 모두 이 근본적인 차이에서 비롯된다.
  • 이로부터 파생되는 여러 차이는 다음과 같다.
  • 먼저 프로세스는 보다 독립적이다. 서로 구분되는 자원을 할당 받아 정말 필요한 경우가 아니면 다른 프로세스에 영향을 미치지 않고 실행된다. 반면 스레드는 프로세스의 하위 집합으로 여러 스레드가 같은 프로세스 자원을 공유하기 때문에 독립적이지 않다. 같은 의미로 프로세스는 보유한 자원에 대한 별개의 주소 공간을 갖지만 스레드는 이 주소 공간을 공유한다.
# run_parallel.py
import my_module
from concurrent.futures import ProcessPoolExecutor
import time

NUMBERS = [
    (1963309, 2265973), (2030677, 3814172),
    (1551645, 2229620), (2039045, 2020802),
    (1823712, 1924928), (2293129, 1020491),
    (1281238, 2273782), (3823812, 4237281),
    (3812741, 4729139), (1292391, 2123811),
]

def main():
    start = time.time()
    pool = ProcessPoolExecutor(max_workers=2) # 이 부분만 바꿈
    results = list(pool.map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f'총 {delta:.3f} 초 걸림')

if __name__ == '__main__':
    main()

총 0.822 초 걸림

  • 부모와 자식 프로세스 사이에 데이터가 오고 갈 때 마다 항상 직렬화와 역직렬화가 일어나야 하므로 Processpoolexecutor를 통해 multiprocessing 모듈을 사용하는 데 따른 추가 비용이 매우 큼

이 방식은 잘 격리 되고 레버리지가 큰 유형에 좋음

  • 격리란 프로그램의 다른 부분과 상태를 공유할 필요가 없는 함수
  • 레버리지란 부모와 자식 사이에 주고 받아야 하는 데이터 크기는 작지만, 이 데이터로 인해 자식 프로세스가 계산해야하는 연산의 양이 상당히 클때
  • 수행해야하는 계산이 이런 특징이 없다면 그리 안빠를 수 있다.
  • 처음에는 multiprocessing을 사용하지 않고 프로그래밍을 하고 속도 향상을 위해서 실행은 할 수 있다.
profile
인공지능 파이팅!

0개의 댓글