본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] multiprocessing.Queue와 queue.Queue 내부 구현의 3가지 결정적 차이와 통신 문제 해결 방법

by Papa Martino V 2026. 3. 18.
728x90

multiprocessing.Queue와 queue.Queue
multiprocessing.Queue와 queue.Queue

 

 

파이썬으로 동시성 프로그래밍을 시작할 때 가장 먼저 접하게 되는 도구가 바로 큐(Queue)입니다. 데이터를 안전하게 주고받기 위한 통로 역할을 하는 이 큐는, 사용하는 모듈에 따라 이름은 같아도 내부 동작 방식은 완전히 딴판입니다. 특히 queue.Queuemultiprocessing.Queue를 혼동하여 사용하면 프로그램이 응답하지 않는 데드락(Deadlock)에 빠지거나, 메모리 오염이 발생할 수 있습니다. 본 포스팅에서는 단순한 사용법을 넘어, 운영체제 수준에서의 메모리 공유 방식과 객체 직렬화 메커니즘을 바탕으로 두 큐의 핵심적인 내부 구현 차이를 심층 분석합니다. 이를 통해 여러분의 파이썬 애플리케이션의 성능을 최적화할 수 있는 해결 방법을 제시합니다.


1. 설계 철학의 근본적인 차이점

두 클래스는 해결하고자 하는 대상이 다릅니다. queue.Queue멀티스레딩(Multi-threading) 환경을 위해 설계되었으며, 동일한 프로세스 내의 메모리 공간을 공유하는 스레드 간의 통신을 담당합니다. 반면, multiprocessing.Queue는 서로 격리된 메모리 공간을 가진 멀티프로세싱(Multi-processing) 환경에서 데이터를 안전하게 전달하기 위해 설계되었습니다.


2. 내부 구현 메커니즘 3가지 상세 비교

개발자가 반드시 알아야 할 두 큐의 기술적 차이를 구조화된 표로 정리하였습니다. 이 차이를 이해하는 것이 버그 없는 병렬 코드를 짜는 첫걸음입니다.

비교 항목 queue.Queue (스레드 전용) multiprocessing.Queue (프로세스 전용)
데이터 전송 방식 메모리 주소 참조 (Shared Memory) 파이프(Pipe)와 피클링(Pickling)
동기화 도구 threading.Lock, Condition OS 수준의 Semaphores, Lock
객체 제약 사항 모든 파이썬 객체 가능 직렬화(Pickle) 가능한 객체만 가능
성능 특성 오버헤드가 매우 낮음 직렬화/역직렬화 비용 발생
주요 장애 시나리오 GIL로 인한 CPU 바운드 작업 병목 대용량 데이터 전송 시 파이프 버퍼 가득 참

3. 심층 분석: 왜 multiprocessing.Queue는 복잡한가?

① 객체의 직렬화(Pickling) 과정

프로세스는 각자의 가상 메모리 공간을 가집니다. 프로세스 A에 있는 객체의 메모리 주소를 프로세스 B에 전달해봐야 B는 그 주소에서 아무것도 찾을 수 없습니다. 따라서 multiprocessing.Queue는 데이터를 보낼 때 객체를 바이트 스트림으로 변환(Pickle)하여 파이프를 통해 던지고, 받는 쪽에서 다시 객체로 복원(Unpickle)합니다. 이 과정에서 람다 함수나 특정 로컬 클래스는 전송이 불가능한 문제가 발생할 수 있습니다.

② 피더 스레드(Feeder Thread)의 존재

multiprocessing.Queue 내부에는 실제 파이프로 데이터를 밀어넣는 별도의 '피더 스레드'가 존재합니다. 데이터를 put() 한다고 해서 즉시 전달되는 것이 아니라, 내부 완충 지대(Buffer)에 머물다가 피더 스레드에 의해 OS 파이프로 이동합니다. 이 메커니즘을 모르면 프로세스 종료 시 데이터가 유실되거나 좀비 프로세스가 생성되는 문제를 겪게 됩니다.


4. Sample Example: 상황별 올바른 사용 방법

두 방식의 차이를 코드 수준에서 명확히 대조해 보겠습니다.

예제 1: 스레드 간 데이터 공유 (queue.Queue)


import queue
import threading

def worker(q):
    item = q.get()
    print(f"스레드 수신: {item}")
    q.task_done()

q = queue.Queue()
q.put("공유 메모리 데이터")
threading.Thread(target=worker, args=(q,), daemon=True).start()
q.join()

예제 2: 프로세스 간 데이터 공유 (multiprocessing.Queue)


import multiprocessing

def worker(q):
    # 이 함수는 독립된 메모리 공간에서 실행됨
    item = q.get()
    print(f"프로세스 수신: {item}")

if __name__ == "__main__":
    # 프로세스 큐는 반드시 __main__ 가드 안에서 사용 권장
    q = multiprocessing.Queue()
    p = multiprocessing.Process(target=worker, args=(q,))
    p.start()
    q.put("직렬화된 데이터")
    p.join()

5. 데드락 해결을 위한 전문가의 팁

멀티프로세싱 환경에서 가장 흔한 오류는 대량의 데이터를 큐에 넣고 join()을 호출하는 것입니다. 파이프의 버퍼 용량(보통 64KB)이 가득 차면 put()을 수행하는 프로세스는 블로킹됩니다. 이때 부모 프로세스가 자식의 종료를 기다리는 join()을 먼저 호출하면 두 프로세스가 서로를 기다리는 무한 루프에 빠집니다. 이를 해결하려면 데이터를 모두 소진(get)한 뒤에 join을 호출하거나, JoinableQueue를 적절히 활용해야 합니다.


6. 결론

프로세스 간 통신(IPC)은 비용이 비싼 작업입니다. 단순한 상태 공유가 목적이라면 스레드와 queue.Queue를, CPU 집약적인 병렬 처리가 필요하다면 multiprocessing.Queue를 선택하십시오. 내부 구현의 차이를 아는 것이 곧 최적화의 열쇠입니다.


내용 출처

  • Python Official Documentation: "queue — A synchronized queue class"
  • Python Official Documentation: "multiprocessing — Process-based parallelism"
  • High Performance Python (2nd Edition) by Micha Gorelick and Ian Ozsvald
  • Linux Kernel Programming: Pipe and FIFO Implementation details
728x90