요청 200개를 한꺼번에 받으려고 프로그램을 200개 띄우면 무엇이 문제일까요? 손님마다 사무실을 새로 차려 주는 대신, 사무실 하나를 함께 쓰면서 창구에 앉을 사람만 늘리는 방법이 있습니다.
기초에서 먼저 보기컴퓨터는 기다리는 동안 다른 일을 할 수 있을까?웹 서버 하나가 요청 200개를 동시에 처리해야 한다고 해봅시다. 요청마다 프로세스를 새로 만들면 메모리 공간을 200벌 만들어야 하고, 서로 데이터를 나누려면 매번 운영체제를 거쳐야 합니다. 너무 비쌉니다. 그래서 나온 것이 스레드입니다 — 메모리는 함께 쓰고, 실행 흐름만 여러 개 두는 방식입니다.
같은 프로세스 안의 스레드들은 힙과 코드, 열어놓은 파일을 전부 공유합니다. 반면 스택은 각자 하나씩 갖습니다. 당연합니다 — 스레드마다 서로 다른 함수를 실행 중이니, 지역변수와 복귀 주소를 담는 스택은 섞이면 안 됩니다. 그래서 스레드를 하나 더 만든다는 것은 대략 "스택 하나 + 문맥 한 벌을 더 마련한다"는 뜻이고, 프로세스를 만드는 것보다 훨씬 쌉니다.
운영체제 눈에 스레드는 언제나 셋 중 하나입니다. 실행 중(Running)은 지금 CPU를 점유한 상태, 준비(Ready)는 일할 수 있는데 순서를 기다리는 상태, 대기(Waiting)는 IO 결과를 기다리며 잠든 상태입니다. 7단원에서 본 그 "기다림"이 Waiting입니다. 6단원에서 본 컨텍스트 스위치는 결국 이 상태들 사이를 옮겨 다니는 일이었습니다.
이 문장이 이 시리즈에서 가장 자주 오해받는 지점입니다. Waiting 상태의 스레드는 스케줄러의 후보에서 빠져 있으므로 CPU를 조금도 쓰지 않습니다. 그래서 "서버가 완전히 막혀 아무 요청도 못 받는데 CPU 사용률은 10%"라는 상황이 실제로 벌어집니다. 막힌 원인은 CPU가 아니라 스레드가 전부 잠들어 있다는 것입니다.
힙을 공유한다는 것은 편리하지만 위험합니다. 두 스레드가 같은 변수를 동시에 고치면, 6단원에서 본 것처럼 언제 교대가 일어날지 모르기 때문에 결과가 실행할 때마다 달라질 수 있습니다. 이것을 경쟁 상태(race condition)라고 하고, 그래서 잠금(lock) 같은 장치가 필요해집니다. 격리를 포기하고 얻은 가벼움의 대가입니다.
스레드를 만드는 비용은 프로세스보다 훨씬 쌉니다. 그러나 공짜는 아닙니다. 스택 메모리가 스레드마다 따로 필요하고(관행적으로 수백 KB에서 1MB), 스레드가 늘수록 커널의 실행 가능 목록이 길어져 스케줄링 자체가 느려집니다. 그래서 스레드는 수천 개까지는 현실적이지만 수만 개부터는 무너집니다. 이 한계가 뒤에 나올 C10K 이야기의 핵심입니다.
공유 메모리를 안전하게 쓰려면 락이 필요합니다. 락은 정확성을 사는 대신 병렬성을 팝니다. 한 순간에 한 스레드만 그 구간에 들어갈 수 있으므로, 락이 걸린 구간이 길수록 나머지 스레드는 그냥 줄을 섭니다. 그래서 락은 짧게 잡는 것이 원칙이고, 락을 쥔 채 IO를 기다리는 것은 거의 언제나 사고의 시작입니다.
시뮬레이터에서 해보기
이 단원의 용어
왜 이렇게 발전했나
스레드가 존재하는 이유는 정확히 "프로세스의 비용"입니다. 같은 주소 공간을 재사용하니 생성이 싸고, 힙 공유로 협업이 쉽고, 교대도 메모리 지도를 바꿀 필요가 없어 가볍습니다. 대신 격리를 포기했기 때문에 한 스레드의 잘못이 프로세스 전체를 죽일 수 있습니다. 격리와 공유 사이의 맞교환입니다.
블로킹 = CPU를 쓰는 게 아니라 CPU를 떠나는 것 (Waiting).
남은 문제
교대가 싸졌다고 공짜는 아닙니다. 그럼 스레드를 늘리면 항상 빨라질까요? →