LabHub

운영체제 · 프로세스와 스레드 · 이론

스레드 — 싸게 나누는 대신 지켜야 할 것이 생긴다

LabHub 에서 이어서 보기

한 줄 요약

스레드는 코드, 데이터, 힙, 열린 파일을 공유하고 스택과 레지스터만 따로 갖는 실행 흐름이며, 그 공유가 성능의 이유이자 버그의 이유다.

왜 이게 필요했나

프로세스는 격리를 주지만 그 대가로 통신이 비싸다. 서로 다른 주소 공간이라 데이터를 주고받으려면 파이프나 공유 메모리 같은 별도 장치가 필요하다. 웹 서버처럼 같은 데이터를 여러 요청이 함께 봐야 하는 프로그램에서는 이 비용이 부담스럽다.

스레드는 격리를 포기하는 대신 이 비용을 없앤다. 같은 주소 공간을 쓰므로 포인터 하나만 넘기면 데이터가 공유된다.

어떻게 동작하나

한 프로세스 안의 스레드들이 무엇을 나누는지 정리하면 이렇다.

| 공유하는 것 | 따로 갖는 것 |
| --- | --- |
| 코드 영역, 전역 데이터, 힙 | 스택 |
| 열린 파일 디스크립터 | 레지스터, 프로그램 카운터 |
| 시그널 핸들러, 현재 디렉터리 | 스레드 로컬 저장소 |

스레드 컨텍스트 스위치가 프로세스보다 싼 이유가 여기서 나온다. 주소 공간이 같으므로 페이지 테이블을 갈아 끼울 필요가 없고, TLB 를 통째로 비우지 않아도 된다.

구현 모델도 세 가지로 갈렸다. 사용자 수준에서만 관리하는 다대일 모델은 스위치가 극도로 싸지만 한 스레드가 블로킹 시스템 콜을 하면 프로세스 전체가 멈춘다. 리눅스와 윈도우가 채택한 일대일 모델은 사용자 스레드마다 커널 스레드가 대응해 진짜 병렬 실행과 독립적인 블로킹이 가능하지만, 스레드 생성 비용이 상대적으로 크다. 다대다 모델은 둘의 절충인데 구현이 복잡해 주류가 되지 못했다. 요즘의 고루틴이나 코루틴은 이 논쟁을 언어 런타임 층에서 다시 푸는 시도로 볼 수 있다.

현장에서 만나는 모습

스레드를 쓰는 순간 생기는 새 책임이 있다. 공유 데이터에 대한 동기화다. 두 스레드가 같은 변수를 증가시키는 코드는 기계어로 보면 읽고, 더하고, 쓰는 세 단계이며 그 사이에 스위치가 끼면 갱신 하나가 사라진다. 이 문제는 테스트에서 잘 안 잡히고 부하가 올라갈 때만 나타난다는 특징이 있다.

또 하나. "스레드를 늘리면 빨라진다"는 직관은 CPU 코어 수를 넘어서면 무너진다. 코어보다 많은 실행 가능 스레드는 서로를 밀어내며 컨텍스트 스위치와 캐시 오염만 늘린다. CPU 바운드 작업의 스레드 수는 코어 수 근처가 출발점이고, I/O 바운드라면 대기 시간을 감안해 더 늘릴 수 있다. 이 구분 없이 "스레드 풀 크기 200" 같은 설정을 복사해 오는 것이 흔한 사고의 원인이다.

이어지는 퀴즈에서 확인할 것

스레드가 무엇을 공유하고 무엇을 따로 갖는지, 그리고 그 선택이 어떤 버그를 부르는지 구분할 수 있는지 확인한다.