[Python] Multiprocessing
Python Multiprocessing: Shared Memory, Race Condition, and Lock
파이썬은 기본적으로 GIL(Global Interpreter Lock) 때문에 CPU-bound 연산에서는 멀티스레딩이 제대로 성능을 내지 못합니다. 따라서 CPU 코어를 병렬로 활용하기 위해서는 multiprocessing 모듈을 사용하는 것이 일반적입니다. 이 글에서는 파이썬을 이용해서 프로세스 간 데이터 공유 방식, Race Condition 문제, 그리고 Lock을 통한 동기화 기법을 정리합니다.
1. 프로세스 간 메모리 독립성
import multiprocessing
data = []
def append_data():
for i in range(10):
data.append(i)
if __name__ == "__main__":
ps = [multiprocessing.Process(target=append_data) for _ in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(f"data 길이 : {len(data)}")
- 메인 프로세스와 서브 프로세스는 독립적인 메모리 공간을 사용합니다.
- 따라서
data리스트는 공유되지 않으며, 최종 길이는 항상0이 됩니다.
2. Manager를 활용한 프로세스 간 데이터 공유
import multiprocessing
def append_data(shared_data):
for i in range(10):
shared_data.append(i)
if __name__ == "__main__":
with multiprocessing.Manager() as manager:
shared_data = manager.list()
ps = [multiprocessing.Process(target=append_data, args=(shared_data,)) for _ in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(f"data 길이 : {len(shared_data)}")
multiprocessing.Manager()는 별도의 서버 프로세스를 백그라운드로 띄워서 데이터 객체를 관리합니다.- 각 서브 프로세스는 Proxy 객체를 통해 공유 데이터에 접근합니다.
- 실행 결과:
data 길이 : 40.
⚠️ 단점: IPC(Inter-Process Communication)를 통해 동작하기 때문에 성능 오버헤드가 있습니다.
3. 공유 메모리 Array
import multiprocessing
def append_data(shared_data, idx):
for i in range(10):
shared_data[idx+i] = i
if __name__ == "__main__":
shared_data = multiprocessing.Array('i', 40) # 'i'는 C 타입 int
ps = [multiprocessing.Process(target=append_data, args=(shared_data, i*10)) for i in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(f"data 길이: {len(shared_data)}")
multiprocessing.Array는 저수준의 공유 메모리(SHM)를 직접 사용합니다.- IPC 오버헤드가 적고 속도가 빠릅니다.
- 단, 고정 크기만 지원.
4. Race Condition 상황
import multiprocessing
def append_data(shared_data):
for i in range(10):
current = list(shared_data)
current.append(i)
shared_data[:] = current
if __name__ == "__main__":
with multiprocessing.Manager() as manager:
shared_data = manager.list()
ps = [multiprocessing.Process(target=append_data, args=(shared_data,)) for _ in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(f"data 길이 : {len(shared_data)}")
- 각 프로세스가
shared_data를 읽은 뒤 수정하고 다시 덮어쓰는 과정에서 경쟁 상태(Race Condition) 발생. - 실행할 때마다 결과가 다르게 발생.
5. Lock으로 Race Condition 해결
import multiprocessing
def append_data(shared_data, lock):
for i in range(10):
with lock:
current = list(shared_data)
current.append(i)
shared_data[:] = current
if __name__ == "__main__":
with multiprocessing.Manager() as manager:
shared_data = manager.list()
lock = multiprocessing.Lock()
ps = [multiprocessing.Process(target=append_data, args=(shared_data, lock)) for _ in range(4)]
for p in ps:
p.start()
for p in ps:
p.join()
print(f"data 길이 : {len(shared_data)}")
multiprocessing.Lock()은 임계 구역(Critical Section)을 보호합니다.- 하나의 프로세스만 안전하게
shared_data를 수정할 수 있습니다. - 결과는 항상
data 길이 : 40.
요약
- 기본적으로 프로세스 간 메모리는 공유되지 않는다.
Manager→ 편리하지만 느림 (Proxy 기반 IPC).Array→ 빠르지만 크기가 고정.- 동시 접근 시 Race Condition이 발생할 수 있으며,
Lock으로 동기화가 필요하다.
멀티프로세싱을 제대로 활용하려면 작업의 특성(읽기 위주 vs 쓰기 동기화 필요)에 따라 Manager, Array, Lock을 적절히 선택하는 것이 중요합니다.