프로세스 안의 실행 흐름. 같은 주소 공간을 공유한다.
무엇을 나누고 무엇을 따로 갖나
-
공유하는 것 — 코드 · 데이터 · 힙 · 열린 파일 · 시그널 핸들러
-
따로 갖는 것 — 스택 · 레지스터 · 프로그램 카운터 · 스레드 ID
-
힙을 공유한다 = 객체를 그냥 넘길 수 있다 (장점)
- = 동시에 건드리면 깨진다 (단점)
전환이 왜 싼가
프로세스 전환
- 레지스터 저장·복원 + 페이지 테이블 교체 + TLB 플러시
- 캐시·TLB 가 통째로 식는다 (가장 비싼 부분)
스레드 전환
- 같은 주소 공간이라 페이지 테이블 교체가 없다
- TLB 를 그대로 쓴다 → 훨씬 싸다
"스레드가 가볍다"의 실체는 주소 공간 전환이 없다는 것이다.
누가 스케줄하나
커널 스레드 (1:1)
- 커널이 안다 → 멀티코어에 진짜로 분산된다
- 하나가 블로킹돼도 나머지는 돈다
- 대신 생성·전환에 커널이 개입해 비싸다
유저 스레드 (N:1)
- 커널은 프로세스 하나로만 본다 → 라이브러리가 스케줄한다
- 전환이 매우 싸다 (커널 개입 없음)
- 치명적 약점: 하나가 블로킹되면 전부 멈춘다
- 멀티코어를 못 쓴다
M:N (하이브리드)
- 유저 스레드 M 개를 커널 스레드 N 개에 매핑한다
- 둘의 장점을 노리지만 구현이 복잡하다
ps -eLf | head # 스레드까지 보기 (LWP 열)
cat /proc/<pid>/status | grep Threads
top -H -p <pid> # 스레드별 CPU
얼마나 만들 수 있나
스레드 하나 = 스택 (기본 8MB 예약, 실제 사용분만 물리 할당)
-
- 커널의 관리 구조
수천 개면 메모리와 스케줄링이 부담이 된다
- 그래서 "연결마다 스레드" 모델이 대규모 동시성에서 무너진다
ulimit -s # 스레드 스택 크기
cat /proc/sys/kernel/threads-max # 시스템 전체 상한
면접 함정
- ❌ "스레드가 프로세스보다 항상 낫다" → 하나가 죽으면 프로세스 전체가 죽는다. 격리가 필요하면 프로세스다.
- ❌ "스레드는 스택도 공유한다" → 스택은 각자 갖는다. 그래서 지역 변수는 안전하다.
스레드가 많을 때 무엇이 문제인가
- 메모리 — 스택 예약 (기본 8MB 가상, 실사용은 적지만 주소 공간을 먹는다)
- 32비트에서는 이것만으로 한계에 닿는다
- 스케줄링 준비 큐가 길어지고 컨텍스트 스위치가 폭증한다
- 캐시 — 각 스레드의 작업 데이터가 캐시를 밀어낸다
ulimit -s 512 # 스택을 줄여 더 많이 만들 수 있게 (KB 단위)
cat /proc/<pid>/status | grep -E 'Threads|VmRSS'
스레드 안전이란
여러 스레드가 동시에 호출해도 올바르게 동작하는 것
안전하게 만드는 세 가지
-
① 공유하지 않는다 — 스레드 로컬 저장소 · 지역 변수
-
② 공유하되 안 바꾼다 — 불변 데이터
-
③ 공유하고 바꾸되 동기화한다 — 락 · 원자 연산
-
① 이 가장 싸고 ③이 가장 비싸다. 가능하면 위쪽을 택한다
재진입성
시그널 핸들러 안에서는 아무 함수나 부르면 안 된다
→ 그 함수가 실행 중이던 걸 중단하고 들어온 것일 수 있다
→ malloc · printf 는 내부 락을 쓰므로 데드락이 날 수 있다
→ async-signal-safe 목록에 있는 함수만 쓴다 (write · _exit 등)