프로세스와 프로그램의 차이점
프로그램은 단순히 소스 코드 파일(예: main.py)과 같이 저장된 정적 상태를 의미합니다. 반면, 프로세스는 해당 프로그램이 실행되어 운영체제에 의해 메모리 상에 로드되고, 코드와 자원을 활용해 동작하는 동적인 인스턴스입니다. 프로세스는 시스템 자원 할당의 기본 단위이며, 각 프로세스는 독립적인 메모리 공간을 가집니다.
multiprocessing 모듈 소개
Python의 multiprocessing 모듈은 플랫폼 간 호환되는 멀티프로세싱을 지원하며, Process 클래스를 통해 개별 프로세스를 생성할 수 있습니다. 이 방식은 GIL(Global Interpreter Lock)의 제한을 우회하여 CPU 집약적 작업에서 진정한 병렬성을 실현할 수 있게 해줍니다.
기본 사용 예제
다음 코드는 춤추기와 노래 부르기를 각각 별도의 프로세스에서 수행하도록 합니다:
from multiprocessing import Process
import time
import os
def dance():
print(f"춤 시작, PID: {os.getpid()}")
for i in range(5):
print(f"춤 중... {i}")
time.sleep(0.5)
print("춤 끝")
def sing():
print(f"노래 시작, PID: {os.getpid()}")
for i in range(5):
print(f"노래 중... {i}")
time.sleep(0.5)
print("노래 끝")
if __name__ == "__main__":
p1 = Process(target=dance)
p2 = Process(target=sing)
p1.start()
p2.start()
p1.join()
p2.join()
이 코드는 두 작업이 동시에 실행되며, 각각의 출력이 교차해서 나타납니다.
Windows 환경에서의 주의사항
특정 IDE(예: IDLE, Sublime Text 등)에서는 multiprocessing이 정상적으로 작동하지 않을 수 있습니다. 이 문제는 일반적으로 Python 스크립트를 명령줄(CMD 또는 PowerShell)에서 직접 실행함으로써 해결됩니다. Windows는 프로세스 생성 시 spawn 방식을 사용하므로, if __name__ == "__main__": 보호 조건이 반드시 필요합니다.
프로세스에 인자 전달
함수에 매개변수를 전달하려면 args와 kwargs를 사용합니다:
def perform_activity(name, count, **info):
print(f"{name}({info['age']}세)가 활동 시작, PID: {os.getpid()}")
for i in range(count):
print(f"{name}: {i}단계 수행")
time.sleep(0.5)
if __name__ == "__main__":
p1 = Process(target=perform_activity, args=("철수", 4), kwargs={"age": 12})
p2 = Process(target=perform_activity, args=("영희", 6), kwargs={"age": 14})
p1.start()
p2.start()
p1.join()
p2.join()
프로세스 간 통신 (IPC)
서로 다른 프로세스는 메모리를 공유하지 않으므로, 데이터를 주고받기 위해 Queue 객체를 사용해야 합니다. multiprocessing.Queue는 프로세스 간 안전한 데이터 전송을 제공합니다.
from multiprocessing import Process, Queue
import time
def producer(q):
for item in [10, 20, 30, 40]:
q.put(item)
print(f"생산: {item}")
time.sleep(0.6)
def consumer(q):
while not q.empty():
value = q.get()
print(f"소비: {value}")
time.sleep(0.6)
if __name__ == "__main__":
queue = Queue()
p1 = Process(target=producer, args=(queue,))
p2 = Process(target=consumer, args=(queue,))
p1.start()
p1.join() # 생산 완료 후 소비 시작
p2.start()
p2.join()
프로세스 풀 (Process Pool)
수십 개 이상의 프로세스를 생성해야 하는 경우, 매번 개별적으로 생성하는 것은 비효율적입니다. 이때 Pool을 사용하면 최대 프로세스 수를 제한하면서도 작업을 효율적으로 분배할 수 있습니다.
from multiprocessing import Pool
import os
import time
import random
def task(name):
start = time.time()
print(f"[PID: {os.getpid()}] 작업 {name} 시작")
time.sleep(random.uniform(1, 3))
try:
result = "완료"
print(f"[PID: {os.getpid()}] 작업 {name} 성공")
except Exception as e:
print(f"[PID: {os.getpid()}] 작업 {name} 실패: {e}")
end = time.time()
print(f"[PID: {os.getpid()}] 작업 {name} 종료, 소요시간: {end - start:.2f}s")
if __name__ == "__main__":
with Pool(processes=4) as pool:
tasks = [f"작업_{i}" for i in range(1, 9)]
pool.map(task, tasks)
위 예제에서는 최대 4개의 프로세스만을 사용하여 총 8개의 작업을 순차적으로 처리합니다. 하나의 작업이 끝나야 다음 작업이 시작됩니다.
프로세스 풀 내에서의 큐 사용
Pool을 사용할 때는 일반 multiprocessing.Queue() 대신 multiprocessing.Manager().Queue()를 사용해야 합니다. 그렇지 않으면 다음과 같은 런타임 오류가 발생합니다:
RuntimeError: Queue objects should only be shared between processes through inheritance.
이는 Pool이 프로세스를 복제(spool)하는 방식 때문이며, Manager는 프로세스 간 공유 가능한 객체를 생성해 줍니다.
프로세스 vs 스레드 비교
| 구분 | 프로세스 | 스레드 |
|---|---|---|
| 자원 소유 | 독립적인 메모리 공간과 시스템 자원을 가짐 | 부모 프로세스의 자원을 공유 |
| 생성 오버헤드 | 크고 느림 | 작고 빠름 |
| 보안성 | 높음 (격리됨) | 낮음 (공유 메모리) |
| CPU 활용 | GIL 영향 없음, 진정한 병렬 처리 가능 | GIL로 인해 I/O 병렬만 가능 |
| 사용 목적 | CPU 집약적 작업 | I/O 집약적 작업 |
일반적으로 CPU 바운드 작업에는 multiprocessing, I/O 바운드 작업에는 threading 또는 asyncio를 사용하는 것이 적절합니다.