1. 멀티프로세스 (Multi-processing)
프로세스는 실행 중인 프로그램의 인스턴스로, 각 프로세스는 독립된 메모리 공간을 가집니다. 파이썬에서는 multiprocessing 모듈을 통해 병렬 처리를 구현할 수 있습니다.
프로세스 생성의 두 가지 방식
첫 번째 방식은 Process 클래스를 직접 인스턴스화하여 대상 함수를 지정하는 방식이며, 두 번째 방식은 Process 클래스를 상속받아 run 메서드를 오버라이딩하는 방식입니다.
from multiprocessing import Process
import time
import os
# 방식 1: 함수 기반 호출
def execute_work(label):
print(f"작업 시작: {label} (PID: {os.getpid()})")
time.sleep(2)
print(f"작업 완료: {label}")
# 방식 2: 클래스 상속 기반
class CustomWorker(Process):
def __init__(self, identifier):
super().__init__()
self.identifier = identifier
def run(self):
print(f"워커 프로세스 {self.identifier} 가동 중...")
time.sleep(2)
print(f"워커 프로세스 {self.identifier} 종료")
if __name__ == '__main__':
# 방식 1 실행
p1 = Process(target=execute_work, args=('SubProcess-1',))
p1.start()
# 방식 2 실행
p2 = CustomWorker('Worker-01')
p2.start()
p1.join()
p2.join()
print("메인 프로세스 종료")
프로세스 간 메모리 독립성
프로세스는 메모리 공간을 공유하지 않습니다. 자식 프로세스에서 전역 변수를 수정하더라도 부모 프로세스의 변수에는 영향을 주지 않습니다.
from multiprocessing import Process
base_value = 1000
def modify_data():
global base_value
base_value = 0
print(f"자식 프로세스 내부 값: {base_value}")
if __name__ == '__main__':
proc = Process(target=modify_data)
proc.start()
proc.join()
print(f"메인 프로세스 유지 값: {base_value}") # 결과는 여전히 1000
좀비 프로세스와 고아 프로세스
- 좀비 프로세스: 자식 프로세스가 종료되었지만 부모 프로세스가 자원의 상태를 회수(join 등)하지 않아 시스템 테이블에 남아있는 상태입니다.
- 고아 프로세스: 부모 프로세스가 먼저 종료되어 자식 프로세스가 남겨진 경우입니다. 이 경우 운영체제(init 프로세스)가 자식을 입양하여 자원을 회수합니다.
데몬 프로세스 (Daemon Process)
부모 프로세스가 종료될 때 함께 강제 종료되는 프로세스입니다. p.daemon = True 설정을 통해 활성화하며, 반드시 start() 호출 전에 설정해야 합니다.
2. 프로세스 동기화와 통신 (IPC)
여러 프로세스가 동시에 동일한 자원에 접근할 때 데이터 무결성을 지키기 위해 Mutex Lock을 사용하거나, 데이터를 주고받기 위해 Queue를 사용합니다.
상호 배제 (Lock) 활용
from multiprocessing import Process, Lock
import json
import time
def update_inventory(lock, user):
lock.acquire()
try:
# 데이터 읽기 및 수정 시뮬레이션
with open('stock.json', 'r') as f:
db = json.load(f)
if db['count'] > 0:
db['count'] -= 1
time.sleep(0.1) # 네트워크 지연 가정
with open('stock.json', 'w') as f:
json.dump(db, f)
print(f"{user} 구매 성공")
else:
print(f"{user} 재고 부족")
finally:
lock.release()
if __name__ == '__main__':
mutex = Lock()
for i in range(5):
Process(target=update_inventory, args=(mutex, f"User-{i}")).start()
3. 멀티스레드 (Multi-threading)
스레드는 프로세스 내부의 실행 단위입니다. 같은 프로세스 내의 스레드들은 메모리 공간을 공유하므로 통신이 빠르지만, 동기화 문제에 유의해야 합니다.
from threading import Thread, Lock
import time
shared_resource = 0
thread_lock = Lock()
def increase_counter():
global shared_resource
with thread_lock:
current = shared_resource
time.sleep(0.01)
shared_resource = current + 1
if __name__ == '__main__':
threads = []
for _ in range(50):
t = Thread(target=increase_counter)
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"최종 카운트: {shared_resource}")
4. GIL (Global Interpreter Lock)과 성능
CPython 인터프리터는 한 번에 하나의 스레드만 바이트코드를 실행하도록 보장하는 GIL을 사용합니다. 이로 인해 멀티 코어 환경에서도 멀티스레딩이 CPU 연산 작업에서 큰 성능 향상을 내지 못할 수 있습니다.
- CPU 집약적 작업: 멀티프로세스 사용 권장 (GIL 우회, 병렬 처리 가능)
- I/O 집약적 작업: 멀티스레드 사용 권장 (대기 시간 동안 다른 스레드 실행 가능)
5. 스레드 풀과 프로세스 풀
시스템 자원을 효율적으로 관리하기 위해 미리 일정한 수의 스레드/프로세스를 만들어 두고 재사용하는 풀(Pool) 방식을 사용합니다.
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch_url(url):
response = requests.get(url)
return f"{url}: {len(response.content)} bytes"
urls = ["https://www.google.com", "https://www.python.org", "https://www.github.com"]
if __name__ == '__main__':
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(fetch_url, urls))
for res in results:
print(res)
6. 코루틴 (Coroutine)
코루틴은 사용자 레벨에서 스케줄링되는 경량 스레드입니다. 단일 스레드 내에서 I/O 발생 시 실행 권한을 전환하여 동시성을 극대화합니다.
from gevent import monkey; monkey.patch_all()
import gevent
import time
def async_task(name, duration):
print(f"{name} 시작")
gevent.sleep(duration)
print(f"{name} 완료")
if __name__ == '__main__':
start = time.time()
gevent.joinall([
gevent.spawn(async_task, 'Task-A', 2),
gevent.spawn(async_task, 'Task-B', 1),
gevent.spawn(async_task, 'Task-C', 3)
])
print(f"전체 소요 시간: {time.time() - start:.2f}초")