Skip to content

Latest commit

 

History

History
126 lines (81 loc) · 7.21 KB

File metadata and controls

126 lines (81 loc) · 7.21 KB

스레드(Thread)

스레드(thread)는 프로세스를 구성하는 실행 흐름의 단위이다.

하나의 프로세스는 하나 이상의 스레드를 가질 수 있다.

일반적인 개념이다. 하지만 최신 CPU 하이퍼스레딩을 통한 스레드를 알고 있는 사람이라면 이런 질문이 가능하다.

"CPU를 설명할 때 쓰는 n코어 m스레드의 스레드가 프로세스의 스레드인가요?"

프로세스의 스레드 즉 소프트웨어적인 개념의 스레드와 CPU의 하드웨어 스레드는 다르다.

하드웨어 스레드는 그 실행을 실제로 처리해주는 자원이며, 소프트웨어 스레드는 그 실행에 해당한다.

하드웨어 스레드는 소프트웨어 스레드를 받아 처리한다.

하드웨어 관점으로의 8코어 16스레드 CPU는 소프트웨어 프로세스 스레드를 동시에 16개 처리가 가능하다는 것이다.

스레드의 구성 요소

스레드 ID, PC를 비롯한 레지스터 값, 스택 등 실행에 필요한 최소한의 정보를 가진다.

중요한 포인트는 하나의 프로세스에 대한 여러 스레드들은 프로세스의 자원(힙, 데이터, 코드 영역)을 공유한다는 것이다.

반면 위에서 언급한 스레드 ID, PC를 비롯한 레지스터 값, 스택 등은 하나의 스레드가 가지는 고유한 구성요소이다.

스레드의 컨텍스트 스위칭

"엥?! 분명 컨텍스트 스위칭은 자원을 받는 프로세스가 다른 프로세스로 전환될 때 발생하는 것으로 알고있는데?

정확히, 컨텍스트 스위칭은 프로세스끼리도 스레드끼리도 일어난다.

서로 다른 프로세스들은 메모리 공간을 공유하지 않는다.

그렇기에 프로세스 간 컨텍스트 스위칭은 하던 작업을 기억해야한다는 문제에 의해 PCB에 이런 저런 값들을 적재하고 다른 프로세스로 넘어가야 했다.

하지만 스레드 간의 컨텍스트 스위칭의 경우 공유하는 영역(코드, 데이터, 힙)과 공유하지 않는 영역이 존재한다.

스레드에 대한 컨텍스트 스위칭시 공유하는 영역의 백업이 필요없기에 프로세스 간 컨텍스트 스위칭보다 비용이 적다.

물론 당연히 같은 프로세스의 스레드들의 컨텍스트 스위칭일 때의 이야기일 뿐이다.

운영체제가 컨텍스트 스위칭 비용을 고려하는 방식

운영체제는 컨텍스트 스위칭의 비용을 많은 기법들을 통해 최소화하려고 노력한다.

우선 현대에 굉장히 보편화된 멀티코어-멀티스레드 CPU를 기준으로 같은 프로세스의 스레드들을 같은 코어에서 실행시키려고 한다.

이렇게 되면 L1/L2 캐시 히트율이 높아져서 성능이 좋아지기 때문이다.

이 이야기의 포인트는 랜덤하게 A프로세스의 A스레드 -> B프로세스의 C스레드와 같이 고비용 컨텍스트 스위칭이 일어나지 않게 최대한 해왔던 일거리를 일터에 보장해준다는 것이다.

운영체제는 단순히 "누가 기다리고 있으니 다음은 얘 실행!"이 아니라,

스위칭 비용, 캐시 친화성, 우선순위, 실시간성, 인터럽트 상태 등 수많은 요소를 종합적으로 고려해서 다음에 실행할 스레드를 고른다.

A프로세스의 1번 스레드가 실행되고 있다. 1번 스레드의 컨텍스트 스위칭이 필요해진 시점에 새로 들어올 스레드는 B,C,D 프로세스의 스레드가 아니라 A프로세스의 다른 스레드가 들어올 확률이 크다는 것이다.

멀티코어 - 멀티스레드 기반의 컨텍스트 스위칭 흐름

┌───────────────────────────┐
│         CPU 구조           │
├────────────┬──────────────┤
│ Core#0     │ Core#1       │  ← 물리 코어
│ ┌───────┐  │ ┌───────┐    │
│ │ HW T0 │  │ │ HW T2 │    │  ← 하드웨어 스레드 (Hyperthreading)
│ └───────┘  │ └───────┘    │
│ ┌───────┐  │ ┌───────┐    │
│ │ HW T1 │  │ │ HW T3 │    │
│ └───────┘  │ └───────┘    │
└────────────┴──────────────┘

[시점 1] A 프로세스의 T0 스레드가 HW T0 위에서 실행 중
────────────────────────────────────────────
HW T0 → A 프로세스의 스레드 A1 실행
         (공유 메모리: 코드, 힙, 데이터)

[시점 2] 스레드 A1이 블로킹 상태로 진입
→ OS 스케줄러가 A 프로세스의 스레드 A2를 선택
────────────────────────────────────────────
HW T0 → 컨텍스트 스위칭 (Same Process)
       A1 → A2 (저렴한 전환 비용)

[시점 3] A 프로세스 전체가 블로킹됨
→ OS는 B 프로세스의 스레드 B1을 선택
────────────────────────────────────────────
HW T0 → 컨텍스트 스위칭 (Process Switch)
       A2 → B1 (주소 공간 교체, 비용 큼)

[시점 4] Core#1에서 동시에 C 프로세스의 스레드 C1 실행 중
→ 진정한 멀티코어 병렬 처리
────────────────────────────────────────────
HW T2 → C 프로세스의 C1 스레드 실행

[결과 요약]
- 같은 프로세스 내 스레드 간 전환은 빠름
- 다른 프로세스 간 전환은 캐시, 주소공간 전환 등으로 느림
- OS는 가능하면 같은 프로세스 내에서 먼저 전환

스레드와 컨텍스트 스위칭이 실무에 주는 시사점

현대의 자바 기반 백엔드 시스템은 수많은 요청을 처리해야 하므로, 스레드 수 조절, 스케줄링 정책 이해, 불필요한 컨텍스트 스위칭 최소화가 성능에 큰 영향을 준다.

예를 들어,

  • I/O 작업이 많은 서비스에서 스레드 수가 적으면 처리량이 낮아지고,
  • 반대로 너무 많으면 컨텍스트 스위칭 비용으로 오히려 느려질 수 있다.
  • Virtual Thread (Java 21~)는 경량 스레드로 수천 개도 가능하므로, 블로킹 코드가 많은 경우 매우 유리하다.

실제 설정 예:

int coreCount = Runtime.getRuntime().availableProcessors();
ExecutorService pool = Executors.newFixedThreadPool(coreCount);  // CPU-bound에 적합

추가로, GC, DB 커넥션 풀, 서블릿 스레드풀 모두 스레드를 사용하기 때문에, 전체 스레드 수와 CPU 리소스를 함께 고려한 튜닝이 필요하다.

OS는 컨텍스트 스위칭 비용을 줄이려 최적화하지만, 개발자도 그 특성을 이해하고 잘 설계해야 한다.

스레드를 과도하게 생성하거나, 스레드풀을 무작정 키우는 건 오히려 역효과를 낸다.

컨텍스트 스위칭은 보이지 않는 비용이지만, 실무에서 성능 튜닝에 핵심이 된다.