Python의 여러 병렬 처리 방법 비교

1. 기준: for 루프

가장 전통적인 방법은 for 루프를 사용한 순차 처리입니다:

from time import perf_counter

class Timer:
    def __init__(self, print_tmpl='{} takes {:.1f} seconds'):
        self.print_tmpl = print_tmpl
    
    def __enter__(self):
        self.start = perf_counter()
        return self
    
    def __exit__(self, *args):
        self.end = perf_counter()
        print(self.print_tmpl.format(self.print_tmpl.split()[0], self.end - self.start))

# 예시 데이터 처리 함수
def process_data(line):
    # 실제 데이터 처리 로직
    return line.strip()

# 데이터 준비
sample_lines = [f"line_{i}" for i in range(10000)]

with Timer(print_tmpl='for loop takes {:.1f} seconds'):
    results = []
    for item in sample_lines:
        processed = process_data(item)
        results.append(processed)
del results

출력 결과:

for loop takes 9.4 seconds

2. map 함수

Python의 map 함수는 for 루프를 간단히 대체할 수 있지만, 본질적으로는 순차 처리입니다:

with Timer(print_tmpl='map takes {:.1f} seconds'):
    results = map(process_data, sample_lines)
    results = list(results)
del results

출력 결과:

map takes 9.2 seconds

3. multiprocessing

multiprocessing은 Python의 다중 프로세스 패키지로, Python 프로그램 내에서 다중 프로세스를 생성하여 작업을 실행하고 병렬 계산을 수행할 수 있습니다.

multiprocessing에는 다양한 복잡한 사용법이 있지만, 이 글에서는 가장 간단하고 편리한 방법을 소개합니다.

(1) multiprocessing.Pool 프로세스에 작용하며, 프로세스 수를 지정할 수 있으며 기본값은 CPU 수입니다.

from multiprocessing import Pool

with Timer(print_tmpl='Pool() takes {:.1f} seconds'):
    with Pool() as p:
        # with Pool(4) as p: # 4개 프로세스 지정
        results = p.map(process_data, sample_lines)
del results

출력 결과:

Pool() takes 2.5 seconds
Pool(4) takes 2.9 seconds
Pool(8) takes 1.8 seconds
Pool(16) takes 1.4 seconds
Pool(32) takes 1.6 seconds

(2) multiprocessing.dummy.Pool 스레드에 작용하며, 사용법은 위와 동일하지만 이 작업에서는 더 느립니다:

from multiprocessing.dummy import Pool as ThreadPool

with Timer(print_tmpl='ThreadPool() takes {:.1f} seconds'):
    with ThreadPool() as p:
        # with ThreadPool(4) as p: # 4개 스레드 지정
        results = p.map(process_data, sample_lines)
del results

출력 결과:

ThreadPool() takes 37.4 seconds
ThreadPool(4) takes 29.4 seconds
ThreadPool(8) takes 33.3 seconds
ThreadPool(16) takes 34.4 seconds

4. p_tqdm

p_tqdm은 pathos.multiprocessing과 tqdm을 감싼 라이브러리로, 병렬 처리에 진행 상황 표시줄을 쉽게 추가할 수 있습니다. 주요 방법은 다음과 같습니다:

병렬 map: p_map - 순서 있는 병렬 map p_umap - 순서 없는 병렬 map 순차 map: t_map - 순서 있는 순차 map 사용법은 map과 동일하며, 여기서는 한 가지를 예시로 보여줍니다:

from p_tqdm import p_map, p_umap, t_map

with Timer(print_tmpl='p_map takes {:.1f} seconds'):
    results = p_map(process_data, sample_lines)
del results

출력 결과:

100%|██████████████████████████████████████| 88880/88880 [05:28<00:00, 270.19it/s]
p_map takes 329.7 seconds
100%|██████████████████████████████████████| 88880/88880 [05:33<00:00, 266.75it/s]
p_umap takes 334.4 seconds
100%|█████████████████████████████████████| 88880/88880 [00:09<00:00, 9530.67it/s]
t_map takes 9.3 seconds

결론:

여러 순차 실행 방법은 시간이 거의 비슷합니다: 기준 for 루프, map, t_map;

프로세스 기반 병렬화 Pool과 스레드 기반 병렬화 ThreadPool 중 어느 것이 더 빠른지는 구체적인 상황에 따라 실험 분석해야 합니다. 이 글에서는 데이터가 SSD에 저장되어 있어 데이터 읽기가 병목이 아니고, 주요 소요 시간이 CPU 처리에 있기 때문에 프로세스 기반이 더 빠르고, 반대로 스레드 기반은 매우 느립니다. 반대로 I/O 집약적인 작업에서는 데이터 읽기가 병목이므로 ThreadPool이 더 빠를 수 있습니다;

프로세스 수나 스레드 수를 너무 크게 또는 너무 작게 설정하면 실행 시간이 길어질 수 있으므로, 자신의 작업과 머신 상황에 따라 적절한 수를 실험적으로 설정해야 합니다;

p_tqdm 라이브러리의 p_map, p_umap은 병렬 처리에 진행 상황 표시줄을 제공하지만 실행 시간이 너무 길어져 권장하지 않습니다. 진행 상황을 표시하고 싶다면 t_map을 순차 처리하거나 tqdm 라이브러리를 직접 사용하는 것이 더 빠릅니다;

태그: multiprocessing p_tqdm threadpool 병렬 처리 Python 성능 최적화

6월 26일 22:46에 게시됨