전체 목록
운영체제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 수준 / multiprocessingthreading, Runnable