
전통적인 방식에서는 웹 서버가 클라이언트 요청을 처리할 때, 요청마다 새로운 프로세스(Process)를 생성했습니다. 예를 들어 웹 서버에 10명의 사용자가 동시에 접속하면, 10개의 프로세스를 만들어야 합니다.

이 작업은 수천~수만 CPU 사이클이 소요되며, 프로세스가 많을수록 전환 빈도가 높아져 실제 작업에 쓸 수 있는 CPU 시간이 줄어듭니다.
이 과정은 매우 무겁고, 요청이 올 때마다 반복하면 성능이 크게 저하됩니다.
핵심: 이 세 가지 문제를 해결하기 위해 등장한 것이 스레드(Thread)입니다.

멀티스레딩이란 하나의 프로세스 안에 여러 개의 실행 흐름(스레드)을 두는 것입니다.
| 구성 요소 | 설명 |
|---|---|
| Thread ID | 스레드를 식별하는 고유 번호 |
| Program Counter (PC) | 다음에 실행할 명령어의 주소 |
| Register Set | CPU 레지스터 값들. 스레드가 계산 중이던 중간 결과 등을 저장 |
| Stack | 함수 호출 시 지역 변수, 매개변수, 리턴 주소 등을 저장하는 영역 |
웹 브라우저 (하나의 프로세스):
이 세 스레드가 동시에 실행되기 때문에, 이미지를 다운로드하는 동안에도 이미 받은 텍스트를 화면에 표시할 수 있습니다. 만약 단일 스레드라면 다운로드가 끝날 때까지 화면이 멈춰 있을 것입니다.
워드 프로세서 (하나의 프로세스):
사용자가 글을 타이핑하는 동안에도 맞춤법 검사가 동시에 돌아갈 수 있는 이유가 바로 멀티스레딩 덕분입니다.

메모리 구조:

실행 방식: Thread0Func과 Thread1Func이 동시에 실행됩니다.
메모리 구조:
a와 Thread 1의 지역변수 b는 서로 접근 불가)
| 항목 | 함수 호출 | 멀티스레딩 |
|---|---|---|
| 다른 함수/스레드의 지역변수 접근 | 불가 | 불가 |
| 전역 변수 접근 | 가능 (공유) | 가능 (공유) |
| 힙 영역 접근 | 가능 (공유) | 가능 (공유) |
| 스택 | 하나의 스택 공유 | 스레드마다 별도 스택 |
| 실행 방식 | 순차 실행 | 동시(concurrent) 실행 |
중요: 전역 변수와 힙을 공유한다는 것은 편리하지만, 동시에 접근할 때 경쟁 조건(race condition)이 발생할 수 있습니다. 예를 들어 두 스레드가 동시에
globalVar++을 실행하면, 값이 2가 아니라 1만 증가할 수 있습니다. 이것이 동기화(synchronization)가 필요한 이유입니다.

LOAD globalVar → register // 메모리에서 레지스터로 읽기
ADD register, 1 // 레지스터에서 +1
STORE register → globalVar // 다시 메모리에 저장

| 공유하는 자원 | 스레드별 독립적인 자원 |
|---|---|
| Code (프로그램 코드) | Stack (스택) |
| Data (전역 변수) | Registers (레지스터) |
| Files (열린 파일) | Program Counter |
| Heap (동적 메모리) | Thread ID |
핵심: 스레드는 프로세스의 자원을 공유하면서, 실행에 필요한 최소한의 것(스택, 레지스터, PC)만 독립적으로 가집니다. 이것이 스레드가 "경량 프로세스(lightweight process)"라고 불리는 이유입니다.

실제 측정 시, 스레드 간 컨텍스트 스위칭은 프로세스 간 전환보다 수십 배 빠릅니다.
일반적으로 스레드 생성은 프로세스 생성보다 10~100배 빠릅니다.
높은 주소 (0xFFF00000)
┌──────────────────┐
│ per-process │
│ kernel stack │
├──────────────────┤
│ red zone │
├──────────────────┤
│ user area │
├──────────────────┤
│ ps_strings struct│
├──────────────────┤
│ signal code │
├──────────────────┤
│ env strings │
├──────────────────┤
│ argv strings │
├──────────────────┤
│ env pointers │
├──────────────────┤
│ argv pointers │
├──────────────────┤
│ argc │
├──────────────────┤
│ user stack ↓ │ ← 아래로 성장
│ │
│ │
│ heap ↑ │ ← 위로 성장
├──────────────────┤
│ bss │ ← 초기화되지 않은 전역변수
├──────────────────┤
│ initialized data │ ← 초기화된 전역변수
├──────────────────┤
│ text │ ← 프로그램 코드
└──────────────────┘
낮은 주소 (0x00000000)
스레드를 추가하면 이 구조에서 스택 영역만 추가됩니다. 나머지는 모두 공유합니다.

프로세스는 두 가지 요소로 구성된 복합 엔티티(compound entity)입니다:
프로세스를 처음 만들면 자동으로 Thread 0 (메인 스레드)이 하나 생깁니다:
Thread 0은 자신만의 signal mask와 kernel stack을 가지고, 프로세스의 메모리와 Open File table을 사용합니다.
Thread 1을 추가로 만들면:
새로 생기는 것: Thread 1의 signal mask, kernel stack
공유하는 것: Memory, Open File table
핵심 정리:
- 실행의 단위는 스레드(Thread)
- 프로세스는 스레드가 실행되는 환경(execution environment)을 제공
- 프로세스를 "공장"이라고 하면, 스레드는 "공장 안의 노동자"에 해당

스레드 라이브러리는 두 가지 방식으로 구현될 수 있습니다
User-level library (사용자 수준 라이브러리)
Kernel-level library (커널 수준 라이브러리)
| 라이브러리 | 수준 | 비고 |
|---|---|---|
| POSIX PThread | 과거 사용자 수준 → 현재 커널 수준 | Unix/Linux 표준 |
| Win32 Thread | 커널 수준 | Windows API |
| Java Thread | 커널 수준 | JVM이 OS 스레드에 매핑 |

특징
동작 방식
사용자 수준 스레드 라이브러리가 자체적으로:
장점
단점

| OS |
|---|
| Windows XP/2000 |
| Solaris |
| Linux |
| Tru64 UNIX |
| Mac OS X |

사용자 스레드(user thread)와 커널 스레드(kernel thread)를 어떻게 매핑하느냐에 따라 네 가지 모델이 있습니다.

User threads
↓ ↓ ↓ ↓ ↓
└──┴──┴──┴──┘
│
▼
[ k ] ← 커널 스레드 1개
여러 사용자 스레드 → 하나의 커널 스레드

한 스레드가 블로킹 시스템 콜(예: 디스크 읽기, 키보드 입력 대기)을 호출하면, 유일한 커널 스레드가 블록되어 프로세스 전체가 멈춤. 나머지 사용자 스레드들도 모두 실행 불가.
Thread A → Disk read (blocking) → 커널 스레드 블록!
Thread B → 실행하고 싶지만 커널 스레드가 없으므로 대기
Thread C → 마찬가지로 대기
또한 커널이 스레드를 하나만 인식하므로 멀티코어 CPU를 활용할 수 없음.
현재는 거의 사용되지 않는 모델입니다.


각 사용자 스레드 → 각각의 커널 스레드
Thread A → Disk read (blocking) → 커널 스레드 A 블록
Thread B → 커널 스레드 B가 있으므로 계속 실행!
Thread C → 커널 스레드 C가 있으므로 계속 실행!
현재 가장 널리 사용되는 모델입니다.

User threads
↓ ↓ ↓ ↓ ↓
└──┴──┼──┴──┘
╱ │ ╲
▼ ▼ ▼
[k] [k] [k] ← 커널 스레드 3개 (사용자 스레드보다 적음)
여러 사용자 스레드 → 여러(더 적은 수의) 커널 스레드
Thread A → Sock read (blocking) → 커널 스레드 1 블록
Thread B → Sock read (blocking) → 커널 스레드 2 블록
Thread C → Disk read (blocking) → 커널 스레드 3 블록
Thread D → 실행하고 싶지만 가용 커널 스레드 없음 → 대기!


Many-to-Many + 특정 스레드의 1:1 바인딩
| 모델 | 매핑 | 블로킹 시 | 멀티코어 활용 | 생성 비용 | 대표 OS |
|---|---|---|---|---|---|
| Many-to-One | M:1 | 전체 프로세스 블록 | 불가 | 매우 낮음 | (과거) Solaris Green |
| One-to-One | 1:1 | 다른 스레드 계속 실행 | 가능 | 높음 | Linux, Windows |
| Many-to-Many | M:N | 커널 스레드 수에 의존 | 제한적 | 중간 | (과거) Solaris |
| Two-level | M:N + 1:1 | 바인딩된 스레드는 보장 | 가능 | 중간 | (과거) IRIX, HP-UX |

스레드 취소란 스레드가 작업을 완료하기 전에 강제로 종료시키는 것입니다.
예를 들어, 웹 브라우저에서 여러 스레드가 이미지를 동시에 다운로드하는 중에 사용자가 "중지" 버튼을 누르면, 남은 다운로드 스레드들을 취소해야 합니다.
대상 스레드를 즉시 종료합니다.
위험성: 스레드가 어떤 작업 중이었는지에 관계없이 즉시 죽이므로:
대상 스레드가 주기적으로 "나를 취소해야 하나?"를 확인(cancellation point)하고, 안전한 시점에서 스스로 종료합니다.
장점: 스레드가 정리 작업(lock 해제, 메모리 반환 등)을 수행한 후 안전하게 종료할 수 있음

Thread 0이 lock을 잡은 상태에서 비동기 취소로 강제 종료되면, unlock()이 실행되지 않습니다.
이때 Thread 1은 어떻게 되는가?
| OS | Thread 1의 상태 |
|---|---|
| Linux | 영원히 잠듦 (deadlock) — lock이 영영 해제되지 않으므로 Thread 1은 무한 대기 |
| Windows | 깨어나서 lock을 획득 — OS가 소유자가 죽은 lock을 자동으로 해제해줌 |

교훈: 비동기 취소는 매우 위험하며, 거의 항상 지연 취소(deferred cancellation)를 사용합니다.

웹 서버가 요청마다 새 스레드를 생성하면:
미리 일정 수의 스레드를 만들어 놓고(pool), 요청이 오면 풀에서 스레드를 꺼내 할당하는 방식입니다.
main :
// 서버 시작할 때 딱 한 번 실행
int main() {
// 스레드 10개를 미리 생성해서 pool에 넣어둠
for (int i = 0; i < 10; i++) {
CreateThread(workThread); // 10개 다 만들어놓음
}
// 이후 요청이 와도 스레드 새로 안 만듦
while(1) {
request = waitForRequest(); // 요청 대기
assignToPool(request); // pool에서 놀고있는 스레드한테 넘김
}
}
각 워커 스레드의 구조:
int workThread(int param) {
while (1) { // 무한 루프
wait for a request to arrive; // 요청 대기
switch (request) { // 요청 종류에 따라 처리
case DB_query:
lock(); // 나 쓰는 동안 다른 스레드 접근 금지
count++;
unlock();
case DB_insert: ...
...
}
}
}
| 장점 | 설명 |
|---|---|
| 빠른 응답 | 이미 만들어진 스레드를 사용하므로, 스레드 생성 시간이 절약됨 |
| 자원 제한 | 스레드 수를 풀 크기로 제한하여 시스템 과부하 방지 |
| 안정성 | 동시 접속자가 폭증해도 스레드 수가 풀 크기를 넘지 않음 |
스레드 풀은 현대 서버 프로그래밍에서 매우 널리 사용됩니다:
ExecutorService / ThreadPoolExecutorconcurrent.futures.ThreadPoolExecutor
POSIX 표준 (IEEE 1003.1c)으로 정의된 스레드 API입니다.
주요 함수:
| 함수 | 설명 |
|---|---|
pthread_create() | 새 스레드 생성 |
pthread_join() | 특정 스레드의 종료를 기다림 |
pthread_exit() | 현재 스레드 종료 |
pthread_cancel() | 다른 스레드 취소 요청 |
pthread_mutex_lock() | 뮤텍스 잠금 |
pthread_mutex_unlock() | 뮤텍스 해제 |

One-to-One 매핑을 사용합니다.
| 구성 요소 | 설명 |
|---|---|
| Thread ID | 고유 식별자 |
| Register Set | CPU 레지스터 값 |
| User Stack | 사용자 모드 스택 |
| Kernel Stack | 커널 모드 스택 |
| Private Data Storage Area | 스레드 전용 데이터 |
이것들을 통칭하여 스레드의 컨텍스트(context)라고 합니다.
| 구조체 | 위치 | 설명 |
|---|---|---|
| ETHREAD (Executive Thread Block) | 커널 공간 | 스레드가 속한 프로세스의 포인터, KTHREAD에 대한 포인터 포함 |
| KTHREAD (Kernel Thread Block) | 커널 공간 | 스케줄링/동기화 정보, 커널 스택, TEB에 대한 포인터 |
| TEB (Thread Environment Block) | 사용자 공간 | Thread ID, 사용자 모드 스택, TLS(Thread-Local Storage) |
ETHREAD와 KTHREAD는 커널 공간에 있으므로 커널만 접근 가능하고, TEB는 사용자 공간에 있으므로 애플리케이션 코드에서도 접근할 수 있습니다.

Linux는 스레드를 "task"라고 부릅니다. Linux에서는 프로세스와 스레드의 구분이 다른 OS만큼 명확하지 않습니다.
스레드 생성은 clone() 시스템 콜을 통해 이루어집니다.
clone()은 fork()와 비슷하지만, 자식 task가 부모의 주소 공간을 공유할지 말지를 플래그로 제어할 수 있습니다.
| 플래그 | 의미 |
|---|---|
CLONE_VM | 부모와 메모리 공간 공유 (이것이 스레드의 핵심!) |
CLONE_FS | 파일 시스템 정보 공유 |
CLONE_FILES | 열린 파일 테이블 공유 |
CLONE_SIGHAND | 시그널 핸들러 공유 |
fork() = clone(플래그 없음) → 완전히 독립적인 프로세스 생성
pthread_create() = clone(CLONE_VM | CLONE_FILES | ...) → 메모리를 공유하는 스레드 생성
Linux의 철학: 프로세스든 스레드든 모두 "task"이고, 자원 공유 정도만 다를 뿐입니다. 커널 스케줄러는 프로세스와 스레드를 구분하지 않고 모두 동일하게 스케줄링합니다.

Futex는 이 점을 이용합니다:
경쟁이 없는 경우 (fast path): 사용자 공간에서 원자적 연산(atomic operation)만으로 lock을 잡습니다. 커널에 전혀 진입하지 않으므로 매우 빠릅니다.
경쟁이 있는 경우 (slow path): 다른 스레드가 이미 lock을 잡고 있으면, 그때서야 futex(FUTEX_WAIT) 시스템 콜로 커널에 진입하여 대기(sleep)합니다. lock을 잡고 있던 스레드가 해제할 때 futex(FUTEX_WAKE)로 대기 중인 스레드를 깨웁니다
Thread A: lock 시도
→ 아무도 안 잡고 있음 → atomic CAS로 바로 획득 (커널 진입 없음!)
Thread B: lock 시도
→ Thread A가 잡고 있음 → futex(FUTEX_WAIT) → 커널에서 sleep
Thread A: unlock
→ futex(FUTEX_WAKE) → Thread B를 깨움
이렇게 하면 경쟁이 없는 일반적인 경우에는 시스템 콜 오버헤드가 전혀 없고, 경쟁이 발생할 때만 커널이 개입합니다. pthread_join()도 대상 스레드가 아직 실행 중이면 futex로 대기하고, 대상 스레드가 종료하면 깨어나서 메모리를 회수하는 방식으로 동작합니다.

| Java 계층 | pthread 계층 | Linux 커널 계층 |
|---|---|---|
| new Thread() | (메모리 할당만) | (없음) |
| thread.start() | pthread_create() | clone() |
| thread.sleep() | pthread_cond_timedwait() | futex(FUTEX_WAIT) |
| thread.join() | pthread_join() | futex(FUTEX_WAIT) |
| (스레드 종료) | pthread_exit() | exit() |
결국 Java → pthread → 시스템 콜이라는 3단계 계층 구조입니다. Java 개발자는 new Thread().start()만 쓰지만, 그 아래에서는 pthread 라이브러리가 동작하고, 더 아래에서는 리눅스 커널의 clone()과 futex가 실제 작업을 수행합니다. Java thread를 "커널 수준 라이브러리"로 분류한 이유이기도 합니다.
스레드는 프로세스 내의 실행 단위이며, 코드/데이터/힙/파일을 공유하고 스택/레지스터/PC만 독립적으로 가짐
스레드가 프로세스보다 가벼운 이유: 컨텍스트 스위칭 오버헤드 적음, 생성 시간 짧음, 메모리 공유로 효율적
멀티스레딩 모델: 현대 OS(Linux, Windows)는 One-to-One 모델을 사용하여 각 사용자 스레드에 커널 스레드를 매핑
스레드 취소는 위험: 비동기 취소는 deadlock을 유발할 수 있으므로 지연 취소를 사용해야 함
스레드 풀: 미리 스레드를 만들어 재사용하여 생성 오버헤드를 줄이고 시스템 안정성을 확보
공유 자원의 양면성: 메모리 공유는 효율적이지만 race condition을 일으킬 수 있으므로 동기화(mutex, semaphore 등)가 필수