icon

안동민 개발노트

4장 : 스레드

스레드의 개념


프로세스가 실행 중인 프로그램이라면, 스레드(Thread)는 프로세스 안에서의 실행 흐름입니다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 이 스레드들은 같은 메모리 공간을 공유하면서 독립적으로 실행됩니다.

왜 프로세스만으로는 부족할까요? 3장에서 다뤘듯이 프로세스는 독립적인 메모리 공간을 가집니다. 프로세스 간 데이터 공유는 IPC(파이프, 공유 메모리, 소켓 등)를 거쳐야 하고, 프로세스 간 전환은 페이지 테이블 교체와 TLB 플러시를 수반합니다. 만약 하나의 프로그램 안에서 여러 일을 동시에 하고 싶다면 — 웹 서버가 여러 요청을 동시에 처리한다거나, 워드프로세서가 화면 렌더링과 맞춤법 검사를 동시에 한다거나 — 프로세스를 여러 개 만드는 것은 무겁고 비효율적입니다. 이 문제를 해결하기 위해 프로세스 안의 경량 실행 단위인 스레드가 등장했습니다.


스레드가 공유하는 것과 고유한 것

같은 프로세스 내의 스레드들이 무엇을 공유하고 무엇을 독립적으로 가지는지를 정확히 이해하는 것이 멀티스레드 프로그래밍의 기초입니다.

공유하는 자원

  • 코드(텍스트) 영역: 모든 스레드가 같은 프로그램 코드를 실행합니다.
  • 데이터 영역: 전역 변수를 모든 스레드가 공유합니다. 이것이 장점이자 위험입니다. 전역 변수 하나를 수정하면 모든 스레드가 즉시 변경된 값을 볼 수 있어 IPC 없이 데이터를 공유하지만, 동시에 수정하면 데이터가 손상됩니다.
  • 힙 영역: malloc()이나 new로 할당한 동적 메모리를 모든 스레드가 접근할 수 있습니다.
  • 열린 파일 디스크립터: 한 스레드가 연 파일을 다른 스레드도 사용할 수 있습니다.
  • 시그널 핸들러: 프로세스 수준에서 등록되므로 모든 스레드가 공유합니다.
  • 가상 주소 공간: 페이지 테이블이 같으므로, 스레드 간 전환 시 TLB가 유효하게 유지됩니다.

스레드 고유 자원

  • 스레드 ID: 프로세스 내에서 각 스레드를 식별합니다. PID와는 다릅니다.
  • 프로그램 카운터(PC): 각 스레드가 코드의 다른 부분을 실행하고 있으므로, 각자의 PC를 가져야 합니다.
  • 레지스터 세트: CPU 레지스터 값은 스레드마다 독립적입니다.
  • 스택: 각 스레드가 독립적인 함수 호출 체인을 가지므로, 별도의 스택이 필요합니다. 스레드 A가 f() → g() → h()를 호출하는 동안, 스레드 B는 x() → y()를 호출할 수 있습니다. 각자의 지역 변수, 반환 주소, 매개변수가 별도의 스택에 저장됩니다.

이 구분을 시각적으로 정리하면 다음과 같습니다.

자원공유 여부의미
코드, 데이터, 힙공유같은 프로그램, 같은 전역 변수, 같은 동적 메모리
파일 디스크립터공유한 스레드가 열면 다른 스레드도 접근 가능
페이지 테이블공유같은 주소 공간 사용 → TLB 유효
PC, 레지스터고유각 스레드가 코드의 다른 부분을 실행
스택고유독립적인 함수 호출 체인
스레드 ID고유식별자

멀티스레드의 장점

응답성(Responsiveness): GUI 애플리케이션에서 핵심입니다. 파일을 다운로드하는 동안 사용자가 버튼을 클릭하면, 다운로드 스레드와 UI 스레드가 분리되어 있으므로 화면이 멈추지 않습니다. 단일 스레드였다면 다운로드가 끝날 때까지 화면이 "응답 없음" 상태가 됩니다. 안드로이드에서는 메인 스레드(UI 스레드)에서 네트워크 요청을 시도하면 NetworkOnMainThreadException이 발생하여 강제로 분리를 요구합니다.

자원 공유(Resource Sharing): 같은 메모리를 공유하므로 IPC 없이 데이터를 주고받습니다. 프로세스 간의 파이프, 소켓, 공유 메모리 설정이 필요 없습니다. 전역 변수 하나를 선언하면 모든 스레드가 접근할 수 있습니다. 물론 동시 접근의 동기화 문제가 따라오지만, 프로세스 간 통신보다 구현이 간단합니다.

경제성(Economy): 스레드 생성은 프로세스 생성보다 10100배 빠릅니다. fork()는 PCB 생성, 메모리 공간 설정(COW 포함), 파일 디스크립터 복사 등이 필요하지만, 스레드 생성은 스택 영역 할당과 TCB(Thread Control Block) 생성만으로 충분합니다. 컨텍스트 스위칭도 프로세스 간 전환보다 25배 빠릅니다. 페이지 테이블을 바꿀 필요가 없어 TLB가 유지되기 때문입니다.

멀티코어 활용(Scalability): 멀티코어 CPU에서 각 스레드가 서로 다른 코어에서 실제로 동시에 실행될 수 있습니다. 4코어 CPU에서 4개의 스레드가 독립적인 계산을 수행하면, 이론적으로 4배 빠릅니다. 단일 스레드 프로세스는 코어가 아무리 많아도 하나의 코어만 사용합니다.

하지만 멀티스레드가 만능은 아닙니다. Amdahl의 법칙에 따르면, 프로그램의 순차적(병렬화 불가능한) 부분이 전체의 P%라면, 코어가 무한히 많아도 속도 향상의 상한은 1P/100\frac{1}{P/100}배입니다. 프로그램의 25%가 순차적이라면, 코어를 아무리 늘려도 최대 4배까지만 빨라집니다.


싱글 스레드 vs 멀티 스레드

웹 서버를 예로 들어 비교해 보겠습니다.

싱글 스레드 서버: 요청이 오면 처리하고, 완료된 후 다음 요청을 처리합니다. 앞의 요청이 데이터베이스 쿼리로 500ms를 기다리는 동안, 뒤의 요청들은 전부 대기합니다. 1초에 2개의 요청만 처리할 수 있습니다.

멀티 스레드 서버 (Apache httpd 모델): 각 요청을 별도 스레드에서 처리합니다. 한 스레드가 DB 응답을 기다리는 동안 CPU는 다른 스레드의 요청을 처리합니다. 동시에 수백~수천 개의 요청을 처리할 수 있습니다.

이벤트 기반 싱글 스레드 (Node.js 모델): 그런데 Node.js는 싱글 스레드인데도 수만 개의 동시 연결을 처리합니다. 이것은 네트워크 트랙의 11장에서 다룬 이벤트 루프비동기 I/O 덕분입니다. I/O를 기다리는 동안 스레드가 블로킹되지 않고, 콜백을 등록한 뒤 다른 작업을 처리합니다. I/O가 완료되면 콜백이 실행됩니다.

멀티스레드와 이벤트 기반은 동시성을 달성하는 서로 다른 전략입니다. 멀티스레드는 여러 스레드가 각각 블로킹 I/O를 수행하고, 이벤트 기반은 하나의 스레드가 논블로킹 I/O로 여러 작업을 인터리빙합니다. 현대의 고성능 서버(예: Nginx)는 두 접근법을 결합합니다. 소수의 워커 스레드(코어 수만큼)가 각각 이벤트 루프를 돌면서 수만 개의 연결을 처리합니다.


동시성 vs 병렬성

이 두 용어를 정확히 구분해야 합니다.

동시성(Concurrency): 여러 작업이 논리적으로 동시에 진행되는 것입니다. 단일 코어에서도 시분할로 동시성을 달성할 수 있습니다. 실제로는 번갈아 실행되지만, 인간의 관점에서는 동시에 진행되는 것처럼 보입니다.

병렬성(Parallelism): 여러 작업이 물리적으로 동시에 실행되는 것입니다. 멀티코어 CPU에서 각 코어가 다른 스레드를 실행할 때 달성됩니다.

동시성은 구조(Structure)의 문제이고, 병렬성은 실행(Execution)의 문제입니다. 동시성 있는 프로그램을 멀티코어에서 실행하면 병렬성도 얻을 수 있지만, 싱글 코어에서 실행하면 동시성만 있고 병렬성은 없습니다.

일상 비유로 설명하면: 한 사람이 요리와 빨래를 번갈아 하면 동시성(한 사람, 두 작업), 두 사람이 각각 요리와 빨래를 하면 병렬성(두 사람, 두 작업)입니다.


멀티스레드 프로그래밍의 도전

스레드가 메모리를 공유하는 장점은 곧 위험이기도 합니다. 6장에서 자세히 다루겠지만, 핵심 문제들을 미리 짚어봅니다.

경합 조건(Race Condition): 두 스레드가 같은 변수를 동시에 수정하면 결과가 예측 불가능합니다. count++라는 단순한 연산도 실제로는 "읽기 → 증가 → 쓰기"의 세 단계이므로, 두 스레드가 동시에 실행하면 하나의 증가가 덮어써질 수 있습니다.

교착 상태(Deadlock): 스레드 A가 자원 1을 잡고 자원 2를 기다리고, 스레드 B가 자원 2를 잡고 자원 1을 기다리면, 둘 다 영원히 멈춥니다.

우선순위 역전(Priority Inversion): 낮은 우선순위 스레드가 높은 우선순위 스레드가 필요한 자원을 잡고 있으면, 높은 우선순위 스레드가 블로킹됩니다.

이 문제들은 공유 메모리라는 장점의 이면입니다. 6장(동기화)과 7장(교착 상태)에서 깊이 다룹니다. 우선 다음 절에서는 스레드가 OS 수준에서 어떻게 구현되는지, 사용자 수준 스레드와 커널 수준 스레드의 매핑 모델을 살펴보겠습니다.

목차