CPU가 실행 중인 작업을 바꿀 때 현재 상태를 저장하고 다음 작업의 상태를 복원하는 과정.
무엇을 저장하나
저장해야 할 것 = "다시 돌아왔을 때 아무 일 없던 것처럼 이어가기 위해 필요한 전부"
- 프로그램 카운터(PC) — 어느 명령을 실행 중이었나
- 범용 레지스터 — 계산 중이던 값들
- 스택 포인터 — 스택의 어디까지 썼나
- 메모리 관리 정보 — 페이지 테이블 위치 (프로세스 전환 시)
이것을 PCB(Process Control Block)에 넣어 둔다.
언제 일어나나
- 타임 슬라이스 소진 — 할당된 시간을 다 씀(선점형 스케줄링)
- I/O 대기 — 디스크·네트워크를 기다리며 자발적으로 CPU 양보
- 더 높은 우선순위 작업 도착
- 인터럽트 발생
왜 비싼가 — 직접 비용보다 간접 비용
직접 비용: 레지스터 저장·복원. 사실 수백 나노초로 그리 크지 않다.
간접 비용이 진짜 문제다.
전환 전: CPU 캐시에 A 작업의 데이터가 가득 → 대부분 캐시 히트 (1~10ns) 전환 후: B 작업이 캐시를 자기 데이터로 덮어씀 A로 돌아옴: 캐시가 비어 있다 → 메인 메모리에서 다시 (100ns~)
이것을 캐시 오염이라 한다. 게다가 프로세스 간 전환이면
- 주소 공간이 바뀜 → 페이지 테이블 교체 → TLB 전체 무효화(flush)
- 이후 모든 메모리 접근이 페이지 테이블을 다시 타야 한다
TLB 미스 비용이 한동안 계속 발생한다. 그래서 프로세스 전환이 스레드 전환보다 훨씬 비싸다.
실제 수치 감각
- 스레드 전환 — 약 1~5μs
- 프로세스 전환 — 약 3~10μs (+ 캐시·TLB 회복 비용)
작아 보이지만 초당 10만 번 일어나면 CPU의 상당 부분을 여기에 쓴다.
스레드가 너무 많으면 — 실무 함의
new ThreadPoolExecutor(500, 500, …) // CPU 코어는 8개인데 스레드 500개
CPU 8개가 500개를 번갈아 처리하려니 전환만 하다 끝난다. 처리량이 오히려 떨어진다.
적정 스레드 수 어림
-
CPU 바운드 작업 — → 코어 수 (또는 코어 수 + 1)
-
I/O 바운드 작업 — → 코어 수 × (1 + 대기시간/연산시간)
-
예: 코어 8개, 요청당 연산 10ms + DB 대기 90ms
- 8 × (1 + 90/10) = 80개
I/O 대기 중에는 CPU를 안 쓰므로 더 많이 띄울 수 있다는 논리다.
확인 방법
vmstat 1 # cs 컬럼 = 초당 컨텍스트 스위치 수
pidstat -w 1 # 프로세스별, cswch(자발적) / nvcswch(비자발적)
비자발적 전환(nvcswch)이 많다면 스레드가 너무 많거나 CPU가 부족한 것이다. 자발적 전환(cswch)이 많다면 I/O나 락 대기가 잦은 것이다.