목차
스레드를 늘리면 스케줄링 벽에 부딪힌다고 했다. CPU는 몇 개뿐인데 스레드는 수천이다. 이 불균형을 OS가 어떻게 다루는지, 그리고 그 대가가 무엇인지가 이 글이다.
CPU는 몇 개뿐인데 스레드는 수천이다
CPU 코어 하나는 한 순간에 딱 하나의 스레드만 실행할 수 있다. 코어가 8개면 진짜로 동시에 도는 스레드는 8개뿐이다. 그런데 서버엔 스레드가 수백, 수천이다. 나머지는 어떻게 도나.
답은 “아주 빠르게 번갈아 태운다”이다. 한 스레드를 잠깐 태우다 내리고, 다음 스레드를 잠깐 태우고, 이걸 눈에 안 보일 만큼 빠르게 반복한다.
아주 빠르게 번갈아 태운다
OS의 스케줄러가 이 교대를 지휘한다. 각 스레드에게 아주 짧은 시간(타임 슬라이스)만큼 CPU를 주고, 그 시간이 지나면 내리고 다음 스레드에게 넘긴다.
코어 1의 시간축 →
[스레드 A][스레드 B][스레드 C][스레드 A][스레드 B] ...
각 조각이 몇 밀리초 - 사람 눈엔 셋이 "동시에" 도는 것처럼 보인다너무 빨라서 우리는 A·B·C가 동시에 도는 것처럼 느낀다. 실제로는 한 코어가 셋을 잘게 번갈아 태우는 것뿐이다. 이 착시가 “한 서버가 요청 수백 개를 동시에 처리한다”의 정체다.
바꿔 태우려면 상태를 갈무리한다
문제는 스레드를 내렸다 나중에 다시 태울 때다. 아까 그 스레드는 “어느 코드까지 실행했고 계산하던 값이 무엇인지”를 갖고 있었다(프로세스가 받는 실행 상태 그것). 내릴 때 이걸 안 챙기면, 다시 태웠을 때 이어서 못 한다.
그래서 스레드를 내릴 때 그 실행 상태(레지스터 등)를 통째로 저장하고, 다시 태울 때 그대로 복원한다. 이 저장·복원이 컨텍스트 스위치(context switch) 다.
스레드 A 내림: A의 실행 상태를 저장 (어디까지 했는지 갈무리)
스레드 B 올림: B의 저장된 상태를 복원 (B가 멈췄던 자리로)
→ B는 자기가 멈춘 적 없는 듯 이어서 실행컨텍스트 스위치는 공짜가 아니다
여기가 핵심이다. 저장하고 복원하는 그 일 자체가 CPU 시간을 먹는다. 그 순간 CPU는 정작 스레드의 일은 안 하고 교대 작업만 한다.
숨은 비용이 하나 더 있다. CPU는 방금 쓰던 데이터를 가까운 캐시에 두고 빨리 쓰는데, 스레드가 바뀌면 그 캐시가 새 스레드의 데이터엔 안 맞는다. 새 스레드는 한동안 느린 메모리에서 데이터를 다시 끌어와야 한다. 그래서 바꿔 태운 직후엔 잠시 느리다.
교대가 잦을수록 이 두 비용이 쌓인다.
그래서 스레드가 많을수록 손해다
스레드가 코어 수보다 조금 많은 건 괜찮다. 하지만 수천 개면? 코어 하나가 수백 스레드를 번갈아 태우느라 교대만 하다 정작 일할 시간이 줄어든다. 일하러 온 사람이 자리 바꾸기에 하루를 다 쓰는 꼴이다.
스레드 적당: [일][일][일][일] 교대 비용 미미
스레드 과다: [일][교][일][교][교][일] 교대(교)가 일 사이를 다 갉아먹음이것이 thread-pool 글에서 “스레드가 무제한이면 비싸다”가 CPU 관점에서 뜻하는 바다. 스택 메모리뿐 아니라, 번갈아 태우는 비용 때문에도 스레드는 많다고 좋은 게 아니다.
동시성과 병렬성은 여기서 갈린다
concurrency 글의 동시성과 병렬성 구분이 여기서 밑바닥을 드러낸다.
- 동시성 - 한 코어가 여러 스레드를 번갈아 태워 “동시처럼” 보이게 하는 것. 코어 하나로도 된다.
- 병렬성 - 코어가 여럿이라 스레드가 진짜로 같은 순간에 도는 것. 코어 수만큼만 가능하다.
번갈아 태우기(컨텍스트 스위치)는 동시성을 만들고, 진짜 병렬성은 코어 개수가 정한다. 개념의 구분은 concurrency 글에 있고, 여기선 그 구분이 CPU 하나냐 여럿이냐에서 온다는 걸 본다.
그래서 풀 크기가 코어 수 언저리다
thread-pool 크기를 얼마로 잡나가 이제 이해된다. 대략 이렇게 갈린다.
- CPU를 계속 쓰는 일(계산 위주) - 코어가 진짜로 돌릴 수 있는 수만큼만 두면 된다. 그보다 많으면 교대 비용만 는다. 그래서 코어 수 언저리.
- 기다리는 일이 많은 일(I/O 위주) - 스레드가 CPU를 쓰기보다 응답을 기다리며 노는 시간이 길어서, 코어 수보다 많이 둬도 된다. 노는 스레드는 교대 비용이 적으니까.
왜 I/O 위주면 스레드가 놀고 더 둬도 되는지 - 그 “기다림”의 정체가 뒤의 블로킹 I/O다.
실무에서: CPU가 바쁜데 일은 안 되는 신호
- CPU는 100%인데 처리량이 안 오른다 - 스레드를 너무 많이 띄워 교대 비용이 일을 갉아먹는 신호일 수 있다. 스레드를 줄이면 오히려 빨라지기도 한다.
- 컨텍스트 스위치 수를 지표로 본다 - 초당 컨텍스트 스위치가 비정상적으로 많으면, 스레드 과다나 락 경합(스레드들이 서로 기다리다 자꾸 교대)을 의심한다.
- 스레드 수는 코어와 일의 성격으로 정한다 - “많을수록 빠르다”가 아니다. 계산 위주면 코어 언저리, 대기 위주면 그보다 넉넉히 - 재보고 맞춘다.
정리
- CPU 코어는 한 순간에 스레드 하나만 돌린다 - OS 스케줄러가 아주 빠르게 번갈아 태워 동시처럼 보이게 한다.
- 바꿔 태우려면 스레드의 실행 상태를 저장·복원하는데(컨텍스트 스위치), 이 일과 캐시가 식는 비용이 공짜가 아니다.
- 그래서 스레드가 너무 많으면 교대만 하다 일할 시간이 준다 - 스레드 풀 크기를 코어와 일의 성격으로 정하는 이유다.
- 동시성(번갈아)과 병렬성(진짜 동시)의 차이도 결국 코어가 하나냐 여럿이냐다.
다음 글은 스레드가 하는 일의 큰 축 - 프로그램이 OS에 일을 시키는 시스템 콜로 간다.