본문 바로가기
개발 언어/Python

6강: 병행성 vs 병렬성 - 파이썬 고급편

by 주호파파 2025. 5. 28.
728x90
반응형

 

6강: 동시 수행의 기술 - threading과 multiprocessing

개요 및 중요성

현대 프로그래밍에서 성능 향상을 위해 필수적인 병행성(Concurrency)과 병렬성(Parallelism) 개념을 학습해보겠습니다. 파이썬에서는 threadingmultiprocessing 모듈을 통해 이를 구현할 수 있으며, 각각의 특성과 GIL(Global Interpreter Lock)의 영향을 이해하는 것이 중요합니다.

핵심 포인트: 병행성은 여러 작업을 번갈아 수행하는 것이고, 병렬성은 실제로 동시에 수행하는 것입니다.

병행성 vs 병렬성의 차이점

병행성(Concurrency)은 여러 작업이 논리적으로 동시에 실행되는 것처럼 보이지만, 실제로는 CPU가 빠르게 작업을 전환하며 처리합니다. 병렬성(Parallelism)은 실제로 여러 CPU 코어에서 동시에 작업이 수행됩니다.

GIL(Global Interpreter Lock)의 영향

파이썬의 GIL은 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 제한합니다. 이는 CPU 바운드 작업에서는 멀티스레딩의 성능 향상을 제한하지만, I/O 바운드 작업에서는 여전히 유효합니다.

# GIL 영향 비교 예시
import threading
import multiprocessing
import time

def cpu_bound_task(n):
    """CPU 집약적 작업"""
    result = 0
    for i in range(n):
        result += i * i
    return result

def io_bound_task(duration):
    """I/O 집약적 작업 시뮬레이션"""
    time.sleep(duration)
    return f"작업 완료: {duration}초"

Threading 모듈 활용

threading 모듈은 I/O 바운드 작업에 적합하며, 다수의 스레드가 동시에 실행되는 것처럼 보이게 합니다.

import threading
import time

def worker(name, duration):
    print(f"스레드 {name} 시작")
    time.sleep(duration)  # I/O 작업 시뮬레이션
    print(f"스레드 {name} 완료")

# 멀티스레딩 실행
threads = []
start_time = time.time()

for i in range(3):
    thread = threading.Thread(target=worker, args=(f"Worker-{i}", 2))
    threads.append(thread)
    thread.start()

# 모든 스레드 완료 대기
for thread in threads:
    thread.join()

print(f"총 실행 시간: {time.time() - start_time:.2f}초")

위 코드는 3개의 스레드가 각각 2초씩 대기하지만, 병행적으로 실행되어 총 실행 시간은 약 2초입니다.

Multiprocessing 모듈 활용

multiprocessing 모듈은 별도의 프로세스를 생성하여 GIL 제약을 우회하므로 CPU 바운드 작업에 적합합니다.

import multiprocessing
import time

def cpu_intensive_work(n):
    """CPU 집약적 작업"""
    result = sum(i * i for i in range(n))
    return result

if __name__ == "__main__":
    # 순차 실행
    start_time = time.time()
    results_sequential = []
    for i in range(4):
        results_sequential.append(cpu_intensive_work(1000000))
    sequential_time = time.time() - start_time
    
    # 병렬 실행
    start_time = time.time()
    with multiprocessing.Pool(processes=4) as pool:
        results_parallel = pool.map(cpu_intensive_work, [1000000] * 4)
    parallel_time = time.time() - start_time
    
    print(f"순차 실행 시간: {sequential_time:.2f}초")
    print(f"병렬 실행 시간: {parallel_time:.2f}초")
    print(f"성능 향상 비율: {sequential_time/parallel_time:.2f}배")

실습: 웹 크롤링 성능 비교

실제 시나리오를 통해 threading과 multiprocessing의 성능 차이를 비교해보겠습니다. 여러 웹 페이지를 가져오는 I/O 바운드 작업을 시뮬레이션합니다.

import threading
import multiprocessing
import time
import requests
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def fetch_url(url):
    """웹 페이지 가져오기 시뮬레이션"""
    try:
        # 실제로는 requests.get(url)을 사용
        # 여기서는 네트워크 지연을 시뮬레이션
        time.sleep(1)  # 1초 네트워크 지연
        return f"성공: {url}"
    except Exception as e:
        return f"실패: {url} - {e}"

def compare_performance():
    urls = [f"http://example{i}.com" for i in range(5)]
    
    # 1. 순차 실행
    start_time = time.time()
    results_sequential = [fetch_url(url) for url in urls]
    sequential_time = time.time() - start_time
    
    # 2. ThreadPoolExecutor 사용
    start_time = time.time()
    with ThreadPoolExecutor(max_workers=5) as executor:
        results_threading = list(executor.map(fetch_url, urls))
    threading_time = time.time() - start_time
    
    # 3. ProcessPoolExecutor 사용
    start_time = time.time()
    with ProcessPoolExecutor(max_workers=5) as executor:
        results_processing = list(executor.map(fetch_url, urls))
    processing_time = time.time() - start_time
    
    print("=== 성능 비교 결과 ===")
    print(f"순차 실행: {sequential_time:.2f}초")
    print(f"Threading: {threading_time:.2f}초 (개선: {sequential_time/threading_time:.1f}배)")
    print(f"Processing: {processing_time:.2f}초 (개선: {sequential_time/processing_time:.1f}배)")

if __name__ == "__main__":
    compare_performance()

코드 실행 방법 및 결과

위 코드를 실행하면 I/O 바운드 작업(네트워크 요청)에서는 threading이 multiprocessing보다 더 효율적임을 확인할 수 있습니다. 프로세스 생성 오버헤드가 크기 때문입니다.

=== 성능 비교 결과 ===
순차 실행: 5.01초
Threading: 1.02초 (개선: 4.9배)
Processing: 1.45초 (개선: 3.5배)

Queue를 활용한 프로세스 간 통신

multiprocessing에서 프로세스 간 안전한 데이터 교환을 위해 Queue를 사용할 수 있습니다.

import multiprocessing
import time

def producer(queue, name):
    """데이터 생산자"""
    for i in range(5):
        item = f"{name}-아이템-{i}"
        queue.put(item)
        print(f"생산: {item}")
        time.sleep(0.5)
    queue.put(None)  # 종료 신호

def consumer(queue, name):
    """데이터 소비자"""
    while True:
        item = queue.get()
        if item is None:
            break
        print(f"{name}가 소비: {item}")
        time.sleep(0.3)

if __name__ == "__main__":
    # Queue 생성
    queue = multiprocessing.Queue()
    
    # 프로세스 생성
    producer_process = multiprocessing.Process(
        target=producer, args=(queue, "생산자1")
    )
    consumer_process = multiprocessing.Process(
        target=consumer, args=(queue, "소비자1")
    )
    
    # 프로세스 시작
    producer_process.start()
    consumer_process.start()
    
    # 프로세스 완료 대기
    producer_process.join()
    consumer_process.join()
    
    print("모든 작업 완료")

마무리

이번 강의에서는 파이썬의 병행성과 병렬성 구현 방법을 학습했습니다. I/O 바운드 작업에는 threading을, CPU 바운드 작업에는 multiprocessing을 사용하는 것이 효과적입니다. GIL의 특성을 이해하고 적절한 도구를 선택하는 것이 성능 최적화의 핵심입니다.

다음 강의에서는 파이썬 표준 라이브러리의 collections 모듈과 고급 데이터 구조들을 살펴보겠습니다.

728x90
반응형