운영체제Medium#33
프로세스(Process)와 스레드(Thread)의 차이점을 설명해주세요.
#OS#프로세스#스레드#멀티스레딩
힌트
메모리 공유, 컨텍스트 스위칭 비용을 생각해보세요.
정답 및 해설
프로세스(Process)와 스레드(Thread)의 차이점을 설명해주세요.
프로세스는 실행 중인 프로그램의 독립적인 인스턴스로, 운영체제로부터 독립된 메모리 공간을 할당받습니다. 스레드는 프로세스 내부의 실행 단위로, 같은 프로세스 안의 여러 스레드는 메모리와 자원을 공유합니다. 멀티태스킹과 동시성 프로그래밍을 이해하는 데 핵심이 되는 개념입니다.
프로세스 (Process)
정의
프로세스는 실행 중인 프로그램입니다. 프로그램 파일이 디스크에 저장된 정적인 코드라면, 프로세스는 그 코드가 메모리에 로드되어 실행되는 동적인 상태입니다.
프로그램 (정적) → 실행 → 프로세스 (동적)
chrome.exe (디스크) → 실행 → Chrome 프로세스 (메모리)
→ 탭마다 별도 프로세스 생성 (Chrome의 경우)
프로세스의 메모리 구조
프로세스는 운영체제로부터 독립된 가상 메모리 공간을 할당받습니다.
높은 주소
┌──────────────┐
│ Stack │ ← 함수 호출 정보, 지역 변수 (높은 주소에서 아래로 성장)
│ ↓ │
│ │
│ ↑ │
│ Heap │ ← 동적 할당 메모리 (낮은 주소에서 위로 성장)
├──────────────┤
│ Data │ ← 전역 변수, 정적 변수
├──────────────┤
│ Code (Text)│ ← 실행 코드 (읽기 전용)
└──────────────┘
낮은 주소
프로세스 상태
생성(New) → 준비(Ready) → 실행(Running) → 종료(Terminated)
↑ ↓
└── 대기(Waiting) ← I/O 요청 등
프로세스 제어 블록 (PCB)
운영체제는 각 프로세스를 PCB(Process Control Block)로 관리합니다.
// PCB 구조 (개념적)
struct PCB {
int pid; // 프로세스 ID
int state; // 현재 상태 (실행/대기/종료 등)
int program_counter; // 다음 실행할 명령어 주소
int registers[16]; // CPU 레지스터 상태
int priority; // 스케줄링 우선순위
struct MemoryInfo memory; // 메모리 정보
struct FileDescriptor *files; // 열린 파일 목록
};
프로세스 간 통신 (IPC)
프로세스는 독립된 메모리 공간을 가지므로, 서로 통신하려면 운영체제가 제공하는 IPC 메커니즘이 필요합니다.
# 파이프(Pipe)를 이용한 프로세스 간 통신
import multiprocessing
def producer(pipe_conn):
for i in range(5):
pipe_conn.send(f"데이터 {i}")
pipe_conn.close()
def consumer(pipe_conn):
while True:
try:
data = pipe_conn.recv()
print(f"수신: {data}")
except EOFError:
break
parent_conn, child_conn = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=producer, args=(child_conn,))
p2 = multiprocessing.Process(target=consumer, args=(parent_conn,))
p1.start()
p2.start()
p1.join()
p2.join()
IPC 방법 종류:
- 파이프(Pipe): 단방향 데이터 스트림
- 소켓(Socket): 네트워크를 통한 통신
- 공유 메모리(Shared Memory): 메모리 공간 직접 공유
- 메시지 큐(Message Queue): 비동기 메시지 전달
- 세마포어(Semaphore): 동기화 도구
스레드 (Thread)
정의
스레드는 프로세스 내에서 실행되는 경량 실행 단위입니다. 같은 프로세스의 스레드들은 코드, 데이터 영역, 힙을 공유하지만, 각자 독립적인 스택과 레지스터를 가집니다.
스레드의 메모리 구조
프로세스 메모리
┌──────────────────────────────┐
│ Code (공유) │
├──────────────────────────────┤
│ Data (공유) │
├──────────────────────────────┤
│ Heap (공유) │
├──────────┬──────────┬────────┤
│ Stack T1 │ Stack T2 │Stack T3│ ← 각 스레드별 독립
├──────────┼──────────┼────────┤
│ Regs T1 │ Regs T2 │Regs T3 │ ← 각 스레드별 독립
└──────────┴──────────┴────────┘
T1, T2, T3 = Thread 1, Thread 2, Thread 3
스레드 구현
import threading
import time
# 공유 자원
counter = 0
lock = threading.Lock() # 동기화 도구
def increment(n, thread_name):
global counter
for _ in range(n):
with lock: # 임계 구역 보호
counter += 1
print(f"{thread_name} 완료, counter={counter}")
# 두 스레드가 같은 counter를 공유
t1 = threading.Thread(target=increment, args=(1000, "Thread-1"))
t2 = threading.Thread(target=increment, args=(1000, "Thread-2"))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"최종 counter: {counter}") # 2000 (lock 없으면 불일치 가능)
// Java에서의 스레드
public class ThreadExample {
private int counter = 0;
public synchronized void increment() { // synchronized로 동기화
counter++;
}
public static void main(String[] args) throws InterruptedException {
ThreadExample example = new ThreadExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("최종 counter: " + example.counter); // 2000
}
}
스레드의 장단점
장점:
- 생성/종료 오버헤드가 프로세스보다 작음
- 스레드 간 통신이 빠름 (공유 메모리 직접 접근)
- 컨텍스트 스위칭 비용이 낮음
- 같은 작업을 병렬 처리하여 성능 향상 가능
단점:
- 공유 메모리로 인한 동기화 문제 (Race Condition)
- 하나의 스레드 오류가 전체 프로세스에 영향
- 교착상태(Deadlock) 위험
컨텍스트 스위칭 (Context Switching)
CPU가 한 프로세스/스레드에서 다른 것으로 전환할 때 현재 상태를 저장하고 다음 것의 상태를 복원하는 과정입니다.
프로세스 컨텍스트 스위칭:
Process A 실행 → [저장: A의 PCB, 메모리 맵] → [복원: B의 PCB, 메모리 맵] → Process B 실행
스레드 컨텍스트 스위칭:
Thread A 실행 → [저장: A의 레지스터, 스택 포인터] → [복원: B의 레지스터, 스택 포인터] → Thread B 실행
비용 차이:
- 프로세스: 메모리 맵(가상→물리 주소 변환 테이블) 교체 필요 → 비용 큼
- 스레드: 같은 프로세스 내 → 메모리 맵 교체 불필요 → 비용 작음
멀티프로세스 vs 멀티스레드
# 멀티프로세스 - CPU 바운드 작업에 유리 (GIL 우회)
import multiprocessing
def cpu_intensive(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(cpu_intensive, [10**6, 10**6, 10**6, 10**6])
print(sum(results))
# 멀티스레드 - I/O 바운드 작업에 유리
import threading
import requests
def fetch_url(url):
response = requests.get(url)
print(f"{url}: {response.status_code}")
urls = ["https://example.com", "https://google.com", "https://github.com"]
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
Python의 GIL (Global Interpreter Lock)
Python의 특수한 상황:
GIL = 한 번에 하나의 스레드만 Python 코드를 실행할 수 있는 잠금
→ CPU 바운드 작업: 멀티스레딩 효과 없음 (GIL이 병렬 실행 막음)
→ I/O 바운드 작업: GIL이 I/O 대기 중 해제 → 멀티스레딩 효과 있음
해결책:
- CPU 바운드: multiprocessing 모듈 사용 (별도 프로세스 = 별도 GIL)
- I/O 바운드: threading 또는 asyncio 사용
동기화 문제
Race Condition (경쟁 조건)
# 잘못된 예: lock 없이 공유 변수 접근
counter = 0
def unsafe_increment():
global counter
temp = counter # 읽기
temp = temp + 1 # 증가
counter = temp # 쓰기
# T1이 읽기 후 T2가 끼어들면 T1의 증가가 무시될 수 있음
# 올바른 예: lock으로 보호
lock = threading.Lock()
def safe_increment():
global counter
with lock:
counter += 1 # 원자적으로 실행
프로세스 vs 스레드 비교표
| 특성 | 프로세스 | 스레드 |
|---|---|---|
| 메모리 공간 | 독립적 (코드/데이터/힙/스택 각자) | 공유 (코드/데이터/힙) + 독립 (스택) |
| 생성 비용 | 높음 (메모리 할당, PCB 생성) | 낮음 (스택/레지스터만 추가) |
| 컨텍스트 스위칭 | 비용 큼 (메모리 맵 교체) | 비용 적음 |
| 통신 방법 | IPC (파이프, 소켓 등) | 공유 메모리 직접 접근 |
| 통신 속도 | 느림 | 빠름 |
| 고립성 | 높음 (다른 프로세스에 영향 없음) | 낮음 (한 스레드 오류가 프로세스 전체에 영향) |
| 동기화 필요성 | 낮음 | 높음 |
| 병렬성 | 진정한 병렬 처리 가능 | CPU에 따라 달라짐 |
| 활용 예 | 독립적인 서비스, Chrome 탭 | 웹 서버 요청 처리, UI + 백그라운드 작업 |
| 언어 지원 | OS 수준 / multiprocessing | threading, Runnable |