스레드에도 종류가 있습니다. 커널이 관리하는 스레드와 사용자 공간 라이브러리가 관리하는 스레드는 성능 특성이 크게 다릅니다. Java 21의 Virtual Thread가 왜 혁신적인지 이해하려면 이 차이를 알아야 합니다.


세 가지 스레드 모델

PLAINTEXT
┌──────────────────────────────────────────────┐
│  1:1 (커널 레벨)    N:1 (유저 레벨)    M:N (하이브리드) │
│                                              │
│  U1 U2 U3          U1 U2 U3         U1 U2 U3 U4 │
│  │  │  │           │  │  │          │  │  │  │   │
│  │  │  │           └──┴──┘          └──┴──┘  │   │
│  K1 K2 K3             K1             K1     K2   │
│                                              │
│  유저 스레드마다     모든 유저 스레드가    M개 유저 스레드가│
│  커널 스레드 1개     커널 스레드 1개에     N개 커널 스레드에│
└──────────────────────────────────────────────┘

스레드 모델 비교


1:1 모델 — 커널 레벨 스레드

각 유저 스레드가 커널 스레드에 1:1 매핑 됩니다.

PLAINTEXT
유저 공간:    Thread A    Thread B    Thread C
                │           │           │
                │           │           │
커널 공간:    KThread A   KThread B   KThread C
                │           │           │
              CPU 코어     CPU 코어    CPU 코어
  • 스레드 생성/관리를 커널이 직접 수행
  • 시스템 콜로 스레드를 생성 (Linux: clone(), Windows: CreateThread())

장점:

  • 멀티코어 활용 가능 — 커널이 각 스레드를 다른 코어에 스케줄링
  • 하나의 스레드가 블로킹 I/O 해도 다른 스레드는 계속 실행

단점:

  • 스레드 생성/전환 비용이 큼 — 시스템 콜 필요
  • 커널 메모리 사용 (스레드당 커널 스택 약 8KB)
  • 수천 개 이상의 스레드는 무거워짐

대표 구현:

  • Linux NPTL (Native POSIX Threads Library)
  • Java Platform Thread (OS 스레드와 1:1)
  • Windows Thread

N:1 모델 — 유저 레벨 스레드

모든 유저 스레드가 하나의 커널 스레드에 매핑 됩니다.

PLAINTEXT
유저 공간:    Thread A    Thread B    Thread C
                │           │           │
            ┌───┴───────────┴───────────┘
            │  유저 공간 스케줄러 (라이브러리)

커널 공간:  KThread 1 (커널은 스레드 1개만 인식)
  • 스레드 관리를 사용자 공간 라이브러리 가 수행
  • 커널은 이 프로세스에 스레드가 여러 개인지 모름

장점:

  • 스레드 전환이 매우 빠름 — 시스템 콜 불필요, 라이브러리 함수 호출
  • 커널 수정 없이 스레드 라이브러리만으로 구현 가능
  • 스레드당 메모리 사용 적음

단점:

  • 멀티코어 활용 불가 — 커널 입장에서는 프로세스 하나
  • 하나가 블록되면 전체 블록 — 커널 스레드가 1개이므로

대표 구현:

  • GNU Pth (POSIX Thread 라이브러리)
  • 초기 Java Green Thread (Java 1.1)
  • Ruby 1.8 이전의 Thread

M:N 모델 — 하이브리드

M개의 유저 스레드를 N개의 커널 스레드에 매핑 (M > N)합니다.

PLAINTEXT
유저 공간:   Thread1  Thread2  Thread3  Thread4  Thread5
                │       │        │        │        │
            ┌───┴───────┴────────┴────────┴────────┘
            │   유저 공간 스케줄러 (런타임)

커널 공간:  KThread1   KThread2
               │          │
            CPU 코어    CPU 코어

유저 공간 스케줄러가 유저 스레드를 커널 스레드에 동적으로 배분합니다.

장점:

  • 멀티코어 활용 가능
  • 유저 스레드 전환은 빠름 (유저 공간에서 처리)
  • 블로킹 발생 시 해당 커널 스레드만 블록 → 다른 유저 스레드는 다른 커널 스레드로

단점:

  • 구현이 매우 복잡 — 유저 스케줄러 + 커널 스케줄러 2단계
  • 디버깅이 어려움

대표 구현:

  • Go의 goroutine (가장 성공적인 M:N 구현)
  • Java 21 Virtual Thread (M:N 모델 기반)
  • Erlang 프로세스
  • Kotlin Coroutine (디스패처를 통한 M:N)

Go goroutine의 M:N 모델

Go는 M:N 모델의 가장 대표적인 성공 사례입니다.

PLAINTEXT
Go 런타임의 GMP 모델:
G = Goroutine (유저 레벨 스레드, 초기 스택 2KB)
M = Machine (OS 스레드, 커널 스레드)
P = Processor (논리적 프로세서, GOMAXPROCS로 설정)

G1 G2 G3 G4 ... (수만~수백만 개)
  │  │  │  │
  └──┴──┴──┘

  P1    P2  (GOMAXPROCS 개)
  │      │
  M1    M2  (OS 스레드)
  • goroutine은 초기 스택 2KB (OS 스레드는 보통 1~8MB)
  • 수십만 개의 goroutine을 생성해도 문제없음
  • 블로킹 I/O 시 해당 M만 블록되고, P는 다른 M에 연결

Java Virtual Thread

Java 21에서 도입된 Virtual Thread는 사실상 M:N 모델입니다.

JAVA
// 기존 Platform Thread (1:1, OS 스레드)
Thread thread = new Thread(() -> {
    // 스레드당 약 1MB 스택
});

// Virtual Thread (M:N)
Thread vThread = Thread.ofVirtual().start(() -> {
    // 가벼운 스택 (수 KB)
    // 블로킹 I/O 시 자동으로 carrier thread에서 unmount
});
항목Platform ThreadVirtual Thread
모델1:1M:N
스택 크기~1MB~수 KB
생성 가능 수수천 개수백만 개
블로킹 시OS 스레드 블록carrier에서 분리
적합한 상황CPU 집약적 작업I/O 집약적 작업

비교 정리

모델비율멀티코어블로킹전환 비용복잡도
1:1유저:커널 = 1:1O독립적높음낮음
N:1유저:커널 = N:1X전체 블록낮음중간
M:N유저:커널 = M:NO독립적낮음높음

핵심 포인트

  • Java Virtual Thread가 해결하는 문제: 기존 1:1 모델에서 스레드 수천 개가 한계 → 수백만 개의 동시 I/O 처리 가능
  • Go goroutine이 빠른 이유: 2KB 스택, 유저 공간 스케줄링, 시스템 콜 없는 전환
  • N:1 모델의 치명적 단점: 블로킹 I/O 하나로 전체 스레드 중단
  • C10K/C10M 문제와 스레드 모델: 1:1 모델로는 수만 연결 처리가 어려움 → M:N이나 이벤트 루프 필요

정리

스레드 모델은 성능 vs 구현 복잡도 의 트레이드오프입니다. 1:1은 단순하지만 무겁고, N:1은 가볍지만 멀티코어를 못 쓰고, M:N은 둘 다 잡지만 구현이 어렵습니다. Go와 Java 21이 M:N 모델을 성공적으로 구현하면서, 대량 동시성이 필요한 서버 프로그래밍의 기본 선택지가 되고 있습니다.

댓글 로딩 중...