스레드 (Thread)
스레드란?
프로세스 내의 실행 흐름 단위. 같은 프로세스의 메모리를 공유.
공유 vs 독립
| 공유 자원 | 독립 자원 |
|---|
| 코드 영역 | 스택 |
| 데이터 영역 | PC (Program Counter) |
| 힙 영역 | 레지스터 |
| 파일 디스크립터 | 스레드 ID |
사용자 스레드 vs 커널 스레드
| 구분 | 사용자 스레드 | 커널 스레드 |
|---|
| 관리 주체 | 라이브러리 | OS 커널 |
| 전환 속도 | 빠름 | 느림 (시스템 콜) |
| 블로킹 | 하나 블록 → 전체 블록 | 독립적 |
동기화 문제
스레드 A 는 잔액에 100을 더하고, 스레드 B 는 50을 뺍니다.
| 순서 | 동작 | 잔액 |
|---|
| 1 | A 가 읽는다 | 1000 |
| 2 | B 가 읽는다 | 1000. 아직 A 가 쓰지 않았다 |
| 3 | A 가 쓴다 | 1100 |
| 4 | B 가 쓴다 | 950. A 의 변경이 사라졌다 |
| → Race Condition 발생. 뮤텍스/세마포어로 해결. | | |
개수를 어떻게 정하나
많이 만들면 빨라질 것 같지만 일의 성격이 상한을 정합니다.
| 일의 성격 | 적정선 | 왜 |
|---|
| 계산이 대부분 | 코어 수 근처 | 그 이상은 번갈아 도는 비용만 든다 |
| 기다림이 대부분 | 코어 수보다 훨씬 많게 | 기다리는 동안 다른 것이 돈다 |
| 섞여 있다 | 성격별로 풀을 나눈다 | 한 풀에 섞으면 느린 쪽이 빠른 쪽을 굶긴다 |
셋째가 실무에서 큰 차이를 만듭니다. 외부 API 를 부르는 작업과 짧은 계산을 같은 풀에 넣으면
외부가 느려질 때 짧은 작업까지 멈춥니다. 나누면 그 전파가 끊깁니다.
나눠 쓰는 값을 다룰 때
값이 그냥 보이는 것이 편함이자 위험입니다.
읽기만 하면 안전하다. 고치는 순간부터 규칙이 필요하다
고치는 쪽이 하나라도 읽는 쪽이 최신을 본다는 보장은 따로 필요하다
잠금 범위는 좁게. 넓으면 병렬성이 사라진다
둘째가 눈에 안 보이는 결함을 만듭니다. 값을 고쳤는데 다른 스레드가 옛 값을 계속 보는
경우이고, 이것은 시험에서 잘 안 잡힙니다.
실무 포인트
- Java:
Thread, ExecutorService (스레드풀)
- Python: GIL 때문에 멀티스레딩 CPU 바운드에 불리
- Go: goroutine (경량 스레드, M:N 모델)
Q.스레드가 공유하는 자원과 독립적인 자원은?
| 공유 | 독립 |
|---|
| 코드 | 스택 |
| 데이터 (전역변수, static) | 레지스터와 프로그램 카운터 |
| 힙 | 스레드 로컬 저장소 |
| 열린 파일과 소켓 | |
핵심은 스택만 따로 갖는다는 점입니다. 함수 호출 흐름이 스레드마다 독립적이어야 하므로 스택은 나눠야 하고, 나머지는 공유해서 생성과 전환이 가볍습니다.
이 구조가 장점이자 위험입니다. 힙과 전역변수를 함께 쓰므로 통신이 쉽지만, 동시에 접근하면 경쟁 조건이 생깁니다. 한 스레드가 잘못된 메모리를 건드리면 프로세스 전체가 죽습니다.
흔한 실수: 힙을 독립 자원으로 답하는 것. 힙은 프로세스 단위로 하나이고, 그래서 한 스레드가 할당한 객체를 다른 스레드가 그대로 씁니다.
Q.사용자 레벨 스레드와 커널 레벨 스레드의 차이는?
커널이 스레드의 존재를 아는지가 다릅니다.
| 항목 | 사용자 레벨 | 커널 레벨 |
|---|
| 관리 주체 | 라이브러리 | 커널 |
| 생성과 전환 비용 | 매우 싸다. 모드 전환이 없다 | 상대적으로 비싸다 |
| 멀티코어 활용 | 어렵다. 커널은 프로세스 하나로 본다 | 가능하다 |
| 하나가 블로킹되면 | 프로세스 전체가 멈춘다 | 그 스레드만 멈춘다 |
두 번째와 네 번째가 결정적입니다. 사용자 레벨만 쓰면 스레드 하나가 파일을 읽는 동안 나머지도 멈춥니다.
현대 운영체제는 대부분 커널 레벨을 쓰고, 그 위에 경량 실행 단위(코루틴, 고루틴, 가상 스레드)를 올리는 방식이 늘고 있습니다. 커널 스레드 몇 개에 수만 개의 논리 흐름을 얹어 두 장점을 합치려는 시도입니다.
흔한 실수: 사용자 레벨을 옛 기술로만 설명하는 것. 지금의 코루틴이 같은 아이디어를 다시 쓰고 있습니다.
Q.스레드풀을 사용하는 이유는?
스레드를 매번 만들고 없애는 비용을 없애고, 동시 실행 수에 상한을 두기 위해서입니다.
| 이유 | 내용 |
|---|
| 생성 비용 제거 | 미리 만들어 두고 재사용한다 |
| 개수 제한 | 요청이 몰려도 스레드가 무한히 늘지 않는다 |
| 자원 보호 | 문맥 교환 폭증과 메모리 고갈을 막는다 |
| 큐잉 | 넘치는 작업을 대기열에 담아 순서대로 처리한다 |
두 번째가 더 중요합니다. 요청마다 스레드를 만들면 동시 접속 2,000에서 실행 흐름이 2,000개가 되고, 커널이 이들을 번갈아 실행하는 비용만으로 CPU 가 소진됩니다.
풀 크기는 작업 성격으로 정합니다.
| 작업 | 크기 기준 |
|---|
| CPU 중심 | 코어 수 안팎 |
| I/O 대기가 긴 작업 | 코어 수보다 크게. 대기 비율만큼 |
흔한 실수: 크면 좋다고 보는 것. CPU 중심 작업에서 스레드를 코어보다 많이 두면 교환 비용만 늘어납니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
운영체제 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.