Threading Models
하나의 주소 공간 내에서 여러 실행 흐름을 물리적으로 구현하고 관리하는 유저 수준, 커널 수준, 그리고 혼합 스레딩 모델의 물리 구조를 다루는 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
operating-systems-system-mechanicsoperating-systemssystem-mechanicsprocessconcurrency-mechanicsthreading-modelsos-processconcurrency10 min read
1. Overview
스레딩 모델과 병렬 기하학(Threading Models)은 하나의 프로세스 주소 공간 안에 여러 실행 흐름을 배치해, 무거운 프로세스 문맥 교환(Context Switch) 비용을 줄이고 멀티코어 CPU를 더 효율적으로 활용하는 동시성(Concurrency)의 코어 아키텍처입니다.
학습자는 스레드가 프로세스의 힙(Heap)과 데이터(Data) 영역은 공유하면서도, 각자 고유한 스택(Stack)과 레지스터(Register) 상태를 가지는 TCB(Thread Control Block)의 이중적 물리 구조를 살펴봅니다. 나아가 커널이 스레드의 존재를 직접 알지 못하는 유저 스레드(User Thread, M<1>1>)부터, OS가 스케줄링을 맡는 커널 스레드(Kernel Thread, 1<1>1>), 그리고 이 둘을 하이브리드로 섞은 Go 언어의 고루틴(Goroutine, M
2. Scope & Boundaries
In-Scope
- 스레드의 물리 구조 (Thread Anatomy): 프로세스(자원 컨테이너) vs 스레드(실행 단위), TCB(Thread Control Block), 공유 자원(Code/Data/Heap)과 비공유 자원(Stack/Registers).
- 스레딩 모델 맵핑 (Thread Models): User-level Thread(M<1>1>, 커널 인지 불가), Kernel-level Thread(1<1>1>, 커널이 직접 관리), Hybrid Thread(M
, 고루틴/LWP). - 동시성 딜레마 (Concurrency Issues): 데이터 레이스(Data Race), 교착 상태(Deadlock), 스레드 세이프티(Thread-Safety), TLS(Thread Local Storage).
- 최신 비동기 패러다임 (Modern Shifts): Node.js의 싱글 스레드 이벤트 루프, 파이썬 GIL(Global Interpreter Lock)의 물리적 한계.
Out-of-Scope
- 커널 동기화 락킹 원시 구현: 스핀락(Spinlock)이나 RCU 등 OS 커널 소스코드 내부 레벨의 락(Lock) 메커니즘 자체 03-01-04. Kernel Synchronization Primitives 영역.
- 분산 시스템 데이터 일관성: 서버 A와 서버 B 사이의 트랜잭션 충돌 문제 04-03-02. Distributed Transactions 영역.
Boundaries
- Process vs. Thread (03-02-01): 프로세스(03-02-01)는 서로 독립된 주소 공간을 가진 실행 단위이고, 스레드(Threading)는 하나의 프로세스 안에서 Heap/Code/Data를 공유하되 Stack과 Register 상태는 각자 보유하는 실행 흐름입니다. 공유 덕분에 통신 비용은 낮아지지만, 동기화가 어긋나면 데이터 레이스와 교착 상태(Deadlock) 위험이 커집니다.
3. Counterexample
- 유저 스레드(M<1>1>)의 블로킹 붕괴 (The Blocking I/O Trap): M<1>1> 모델로 100개의 유저 스레드를 띄운 서버가 있다고 가정합니다. 유저 스레드 100개 중 단 하나가 하드디스크를 읽기 위해 블로킹 I/O 시스템 콜을 호출하면, 커널은 개별 유저 스레드가 아니라 프로세스 1개가 대기 상태에 들어간 것으로 봅니다. 그 결과 나머지 99개의 유저 스레드가 실행 가능하더라도 커널 스케줄러 관점에서는 전체 프로세스가 멈출 수 있습니다. M<1>1> 모델은 유저 공간 전환 비용이 낮은 대신, 블로킹 시스템 콜을 별도로 우회하지 않으면 전체 진행성이 약해집니다.
- 스레드 풀 맹신과 메모리 파괴 (Thread Explosion & Stack Overflow): 웹 요청마다 1<1>1> 커널 스레드를 무제한 생성하면 스레드 스택만으로도 메모리 압박이 빠르게 커집니다. 스레드 하나당 최소 1MB
8MB의 스택(Stack) 메모리를 잡는 환경에서 스레드가 1만 개 생성되면 스택 메모리만 10GB80GB가 필요하고, 결국 커널의 OOM(Out Of Memory) 킬러가 서버 프로세스를 종료할 수 있습니다. 그래서 실무 서버는 보통 스레드 풀(Thread Pool)과 큐를 사용해 동시 실행 수를 제한하고 스레드를 재사용합니다.
4. Prerequisites
- 프로세스 램 구조 (Basic): 힙(Heap)과 스택(Stack) 영역이 램 안에서 어떻게 생겼는지 알아야 스레드가 뭘 공유하는지 이해할 수 있습니다. (03-02-01 Process Lifecycle)
- 컨텍스트 스위치 역학 (Basic): 레지스터 백업(PCB) 메커니즘을 알아야 TCB가 얼마나 가벼운지 비교할 수 있습니다.
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 공유 주소 공간과 독립 실행 문맥, 스레드 해부학 (Thread Anatomy)
- Why to Learn: 무거운 프로세스(Process) 대신 스레드(Thread)를 썼을 때 왜 문맥 교환(Context Switch) 속도가 10배 이상 빨라질 수 있는지, 램(RAM) 상에서 어떤 상태를 공유하고 어떤 상태를 따로 보관하는지 이해하기 위함입니다.
- What to Learn:
- Concepts: TCB(Thread Control Block), 공유 자원(Code, Data, Heap, FD), 비공유 자원(PC, Register, Stack).
- Skills: 스레드 로컬 스토리지(TLS, Thread Local Storage), 메모리 누수 방지.
- Tools: 리눅스
/proc/[pid]/task/. - Trade-offs: 스레드는 한 프로세스의 힙(Heap)과 글로벌 변수(Data)를 공유하므로 포인터 참조만으로 빠르게 데이터를 주고받을 수 있지만, 한 스레드가 C 배열 인덱스를 잘못 다뤄 세그멘테이션 오류(Segmentation Fault)를 일으키면 같은 프로세스의 다른 스레드까지 함께 종료될 수 있습니다.
- How to Learn:
- 1단계: 운영체제가 프로세스 A에서 B로 교체할 때는 가상 메모리 테이블(TLB, Page Table)을 전환하고 캐시 효율이 낮아지는 '무거운 문맥 교환'을 분석합니다.
- 2단계: 프로세스 A 안의 스레드 1에서 스레드 2로 교체할 때는 가상 메모리는 그대로 공유하고, CPU의 레지스터(PC)와 스택 포인터(SP) 값만 TCB에 저장·복원하는 '가벼운 문맥 교환'의 흐름을 살펴봅니다.
- Implement: C/C++(또는 파이썬 래퍼)로 글로벌 변수
int G = 0;을 선언하고 스레드 2개를 띄움. 두 스레드가 별도 큐(Queue)나 IPC 없이G변수의 메모리 주소를 통해 즉시 데이터를 갱신(공유)하는 반면, 각자의 함수 안에 선언된 지역 변수(Stack)는 서로 침범하지 못하는 메모리 바운더리 텍스트 덤프 관측.
Recommended
Core Topic 02: 커널의 묵인과 개입, 스레드 매핑 모델 (User vs Kernel Threads)
- Why to Learn: 자바나 C++로 만든 스레드가 실제로 OS 커널에서 스케줄링되는 커널 스레드인지, 사용자 공간 런타임이 관리하는 유저 스레드인지 그 백엔드 매핑(Mapping) 역학을 이해하기 위해서입니다.
- What to Learn:
- Concepts: M<1>1> 모델(유저 스레드), 1<1>1> 모델(커널 스레드), Pthreads 라이브러리.
- Skills: 시스템 콜 블로킹(Blocking) 시나리오 방어.
- Tools:
strace의clone()시스템 콜 분석. - Trade-offs: 유저 스레드(M<1>1>)는 커널 모드 진입(System Call) 없이 사용자 공간에서 함수 호출처럼 전환하므로 전환 비용이 낮지만, 하나가 블로킹되면 전체 진행성이 멈출 수 있습니다. 1<1>1> 커널 스레드는 한 스레드가 하드디스크 I/O로 대기(Sleep)해도 커널이 다른 스레드에 CPU를 배정할 수 있지만, 스레드 생성과 전환 때마다 커널 모드 진입(Syscall) 비용을 감수해야 합니다.
- How to Learn:
- 1단계: M<1>1> (유저 스레드): 커널 입장에서는 프로세스 1개만 실행되는 것처럼 보이지만, 사용자 공간의 런타임 라이브러리(ex: 옛날 Green Threads)가 타이머와 레지스터 저장·복원을 이용해 여러 유저 스레드를 번갈아 실행하는 방식을 분석합니다.
- 2단계: 1<1>1> (커널 스레드): 오늘날 리눅스(NPTL)와 윈도우가 채택한 방식으로, C에서
pthread_create()를 호출하면clone()시스템 콜을 통해 커널 내부 구조체(task_struct)가 할당되고 OS 스케줄러가 직접 관리하는 흐름을 살펴봅니다.
- Implement: 1<1>1> 모델(파이썬
threading모듈)에서 스레드 A가time.sleep(10)으로 커널 블로킹에 들어가도, 스레드 B가 백그라운드 연산을 계속 수행하는 시스템 콜 독립성 덤프와htop상에서 OS가 이들을 별개의 코어(CPU)에 배정하는 로드 밸런싱 관측.
Practical
Core Topic 03: 하이브리드 스케줄링, M 모델과 고루틴 (M & Goroutines)
- Why to Learn: 초당 수십만 개의 웹 소켓 접속(C10K 문제)을 처리하는 현대 클라우드 서버에서, 무거운 1<1>1> 커널 스레드의 메모리 한계를 줄인 Go 언어의 동시성(Concurrency) 구조를 이해하기 위함입니다.
- What to Learn:
- Concepts: M
모델, 고루틴(Goroutine), LWP(Lightweight Process), 런타임 스케줄러(GMP 모델). - Skills: Work-stealing(일 훔치기) 스케줄링, 비동기 I/O(Epoll)와의 결합.
- Tools: Go 런타임 프로파일러(Pprof).
- Trade-offs: 1<1>1> 모델의 '무거운 생성 비용'과 M<1>1> 모델의 '블로킹 시 전체 정지' 문제를 줄이고 수백만 개의 가벼운 고루틴을 띄울 수 있지만, OS 커널이 담당하던 복잡한 스케줄링 판단을 Go 런타임 엔진이 사용자 공간에서 한 번 더 수행해야 하므로(이중 스케줄러), 바이너리 크기와 런타임 오버헤드가 생기는 타협입니다.
- Concepts: M
- How to Learn:
- 1단계: 물리 코어 개수만큼 커널 스레드(M)를 띄워두고, 사용자 공간의 수많은 고루틴(N)을 그 위에 매핑(M
)하다가, 고루틴 1개가 파일 읽기(Blocking)에 걸리면 Go 런타임이 다른 고루틴을 실행 가능한 커널 스레드로 옮기는 블로킹 회피 흐름을 분석합니다. - 2단계: 특정 커널 스레드 큐에만 일이 몰리고 다른 큐는 비어 있을 때, 유휴 스레드가 다른 큐의 고루틴(일거리)을 가져와 실행하는 워크 스틸링(Work-Stealing) 로드 밸런싱을 살펴봅니다.
- 1단계: 물리 코어 개수만큼 커널 스레드(M)를 띄워두고, 사용자 공간의 수많은 고루틴(N)을 그 위에 매핑(M
- Implement: Go 언어(가상)의 로직 모사 스크립트. 단일 파이썬 루프(이벤트 큐 모사) 안에서 수천 개의 태스크 코루틴(Coroutine,
async/await)을 띄워, 1<1>1> 모델이었다면 스레드 스택 부담이 커졌을 트래픽을 약 50MB 램 안에서 처리하고 수만 번의 문맥 교환을 사용자 레벨에서 수행하는(M<1>1>/M하이브리드) 비동기 엔진 속도 테스트 덤프.
Advanced
Core Topic 04: 락킹(Locking)과 우회 전략, 스레드 세이프티와 GIL (Thread-Safety & GIL)
- Why to Learn: 100개의 스레드가 1개의 글로벌 변수를 동시에 갱신할 때 벌어지는 경쟁 상태(Race Condition)를 막는 스레드 세이프(Thread-Safe) 설계 원칙을 세우고, 파이썬 GIL이 멀티코어 실행을 어떻게 제한하는지 이해하기 위해서입니다.
- What to Learn:
- Concepts: 스레드 세이프티(Thread-Safety), 데이터 레이스(Data Race), 락(Mutex), GIL(Global Interpreter Lock).
- Skills: 재진입성(Reentrancy), 상태 없는(Stateless) 함수 설계.
- Tools: Valgrind (Helgrind) 스레드 충돌 탐지기.
- Trade-offs: 모든 전역 변수에 뮤텍스(Mutex) 락을 걸면 스레드 세이프해지지만, 100개의 스레드가 순서대로 락을 기다리면 병렬성(Parallelism)이 줄어 싱글 스레드보다 느려질 수 있습니다. 결국 상태(State)를 공유하지 않는 불변 객체나 TLS(지역 저장소)를 쓰는 편이 일반적으로 더 안전한 선택입니다.
- How to Learn:
- 1단계: C 함수 안에
static int cnt = 0;을 두는 순간, 수십 개 스레드가 동시에 진입하면cnt값이 손상될 수 있는 스레드 언세이프(Unsafe) 코드가 됩니다. 이를 막으려면 상태를 지역(Local, Stack) 변수로 옮기거나 적절한 동기화를 적용해 '재진입 가능(Reentrant)'하게 만드는 원칙을 분석합니다. - 2단계: 파이썬(CPython)이 C 확장과 내부 객체 관리를 단순화하기 위해 인터프리터에 '글로벌 락(GIL)'을 두면서, CPU-bound 파이썬 스레드가 여러 코어를 충분히 활용하지 못하는 구조와 이를 회피하기 위한 멀티프로세싱(
multiprocessing) 전략을 살펴봅니다.
- 1단계: C 함수 안에
- Implement: 파이썬
threading모듈로 4개의 스레드가 피보나치수열 등 CPU-bound 연산을 수행하는 코드 작성. 코어가 4개(Htop 관측) 있어도 파이썬 프로세스의 CPU 점유율은 약 100%(1코어치)에 머물며 GIL에 의해 직렬화되는 병목 덤프를 증명하고, 이를ProcessPool로 바꿨을 때 점유율이 약 400%까지 올라가는 실행 차이를 비교 출력.
7. Terminology
8. References
Primary
- [P2] SWEBOK v4.0 - Software Construction / Concurrency (Threads) — Structural foundations.
- [P1] CS2023 - OS/Operating System Principles (Concurrency) — Core requirements.
Secondary
- [Programming with POSIX Threads] David R. Butenhof — The multithreading classic.
- [Systems Performance: Enterprise and the Cloud] Brendan Gregg — Threads and CPU physics.
Industry
- [Intel: Introduction to Hyper-Threading Technology] — Hardware threading standards.
- [Microsoft: Threading in the Windows Kernel] — NT thread implementation details.
9. Final Checklist
Primary
- '프로세스'와 '스레드'가 물리 메모리를 대하는 방식(격리 vs 공유)의 차이를 데이터 손상 관점에서 설명 가능한가? (P1)
- 'TCB'에 저장되어야 하는 '최소한의 물리적 정보'가 무엇인지 CPU의 명령어 실행 주기와 연관 지어 입증할 수 있는가? (P1)
Secondary
- 'N<1>1> 스레딩 모델'이 현대의 멀티코어 하드웨어 자원을 왜 100% 활용하지 못하는지 물리적 사상 구조를 근거로 설명할 수 있는가?
- 스레드마다 별도의 '스택 공간'이 할당될 때, 전체 프로세스의 가상 주소 공간에 미치는 물리적 배치를 도출할 수 있는가?
Industry
- 실시간 멀티미디어 인코딩 서버 설계 시, 왜 '커널 수준 스레드'를 써야 하드웨어 가속기(GPU/NPU)를 병렬로 제어할 수 있는지 제안할 수 있는가? (SFIA)
- 스레드 수가 CPU 코어 수를 훨씬 초과할 때, 왜 전체 처리량이 늘지 않고 '지연 시간(Latency)'만 물리적으로 악화되는지 기술할 수 있는가?