본문 바로가기

Developer/Python

[Python] 파이썬 CPU 확장 방법 1 (멀티 스레드, 멀티 프로세스)

CPU 확장 이유?

  • CPU 속도는 무한히 빨라질 수 없으므로, 애플리케이션에 동시성 + 병렬성 도입

CPU 확장 방법

1. 스레드

<장점>

  • 여러 함수를 동시에 실행하기에 좋다
  • 싱글 CPU : 스레드 차례로 실행
  • 멀티 CPU : 여러 CPU에서 스레드 실행

<단점>

  • GIL의 제약으로 완벽한 확장 솔루션이 아님
import threading

def print(line):
    print(line)

# 스레드 실행 후 완료 대기
t = threading.Thread(target=print, args("multithread Hi",))
t.start()
print("threading start")

# 스레드 완료될 때까지 메인 스레드 대기
t.join()
  • 스레드를 데몬으로 돌리면 백그라운드 스레드로 간주됨.
  • 메인 스레드 종료 후 같이 종료
import threading

def print(args):
    print(args)
    
t = threading.Thread(target=print, args("hello",))
t.daemon = True
t.start()

# 데몬으로 실행했기 때문에, join 함수 필요 없음

 

코어 개수만큼 병렬로 실행해보기

import random
import threading

results = []

def compute():
    results.append(sum([random.randint(1,100) for i in range(100000)]))

workers = [threading.Thread(target=compute) for x in range(8)]

for worker in workers:
    worker.start()

for worker in workers:
    worker.join()
    
print("Results: %s"% results)

위의 코드를 실행하면 알겠지만,

코어가 4개라면 최대 400% CPU를 사용할 수 있어야한다.

하지만, 시스템의 CPU에 접근하기 위해서는 CPython의 GIL이 지키고 있는 구간을 통과해야하기 때문에 병목 현상이 발생한다.

  • GIL은 멀티 스레드를 실행할 때, CPython 성능 제한
  • 스레드는 병렬 컴퓨팅, 네트워크, 파일처럼 느린 입출력을 처리할 때 유용
  • 위의 작업은 메인 스레드를 차단하지 않고 병렬 실행 가능

결론 : 여러 CPU를 사용해서 처리량을 늘리기 위해서는 프로세스 사용하는 것이 좋다.

 

2. 프로세스

일정 시간동안 작업 일부를 병렬화할 수 있다면 threading 모듈보다

multiprocessing 모듈과 작업 분기를 통해 CPU 여러 개에 부하를 분산하는 것이 더 좋다.

 

import random
import multiprocessing

def compute(results):
    results.append(sum([random.randint(1,100) for i in range(10000)]))

with multiprocessing.Manager() as manager:
    results = manager.list()
    workers = [multiprocessing.Process(target=compute, args=(results,)) for x in range(8)]
    
    for worker in workers:
        worker.start()
    for worker in workers:
        worker.join()
    print("Results: %s" % results)
  • 다른 프로세스 간에 공유 가능한 데이터가 없으므로 작성하기가 까다로움.
  • 각 프로세스는 새로운 독립적 파이썬이므로, 데이터는 복사되며 각 프로세스는 고유한 전역상태를 가짐
  • multiprocessing.Manager 클래스는 동시 접근에 안전한 공유 데이터 구조를 만든다.

멀티 프로세싱은 pool 매커니즘을 제공한다.

import multiprocessing
import random

def compute(n):
    return sum([random.randint(1,100) for i in range(10000)])

pool = multiprocessing.Pool(processes=8)
print("Results: %s" % pool.map(compute, ranger(8)))

multiprocessing.Pool을 사용하면 프로세스 자동 관리