← Posts

OS · Thread · Concurrency

스레드는 프로세스와 뭐가 다른가 - 스레드

한 프로세스 안에서 여러 일을 동시에 하는 실행 흐름이 스레드다. 같은 주소공간을 공유하되 자기 스택만 따로 갖는다. 이 공유가 왜 스레드를 싸게 만들면서 동시에 공유 상태 문제를 낳는지.

목차
  1. 한 프로세스가 여러 일을 동시에 하려면
  2. 스레드: 같은 집을 공유하는 실행 흐름
  3. 무엇을 공유하고 무엇을 따로 갖나
  4. 그래서 공유 상태 문제가 생긴다
  5. 스레드는 프로세스보다 싸다
  6. 그래도 공짜는 아니다
  7. 왜 무제한이 안 되나
  8. 실무에서: 멈춘 스레드를 들여다본다
  9. 정리

프로세스는 자기만의 메모리를 가진 무거운 실행체였다. 그런데 서버는 한 프로세스 안에서 요청 수백 개를 동시에 처리한다. 그 “동시에”를 맡는 가벼운 실행 흐름이 스레드다.

한 프로세스가 여러 일을 동시에 하려면

요청마다 프로세스를 통째로 새로 띄우는 건 무겁다 - 주소공간을 새로 만들고 자원을 다시 챙겨야 하니까. 대신 한 프로세스 안에서 실행 흐름만 여러 개 두면 훨씬 가볍다. 그 실행 흐름 하나가 스레드(thread) 다.

concurrency 글이 “서버는 스레드로 동시에 처리한다”고 한 그 스레드가 이것이다. 이번 글은 그 스레드가 무엇인지를 판다.

스레드: 같은 집을 공유하는 실행 흐름

프로세스가 집 한 채라면, 스레드는 그 집에 사는 사람들이다. 같은 집(주소공간)을 공유하며, 같은 냉장고(힙)와 같은 현관(열어둔 자원)을 함께 쓴다. 다만 각자 자기 책상(스택)은 따로 가진다.

한 프로세스에 스레드가 여럿이면, 그들은 같은 메모리 안에서 각자 코드를 실행한다. 집은 하나인데 그 안에서 여러 사람이 동시에 움직이는 셈이다.

무엇을 공유하고 무엇을 따로 갖나

이 나눔이 스레드의 모든 성질을 결정한다.

diagramdiagram
  • 함께 쓰는 것 - 코드, , 열어둔 파일·연결. 한 스레드가 힙에 만든 객체를 다른 스레드가 그대로 본다.
  • 따로 갖는 것 - 스택과 실행 상태(레지스터). 각자 어느 함수까지 실행했는지는 자기만의 것이다.

지역변수(스택)는 스레드마다 따로라 안 부딪히고, 힙의 객체는 공유라 함께 본다. 이 한 줄이 다음 두 성질로 곧장 이어진다.

그래서 공유 상태 문제가 생긴다

힙을 공유한다는 건 양날이다. 스레드끼리 데이터를 주고받기 쉽지만, 같은 객체를 동시에 건드리면 서로의 작업이 엉킨다.

이게 concurrency 글의 “문제는 공유 상태다”의 밑바닥이다. 왜 공유되냐면, 스레드가 같은 주소공간을 쓰기 때문이다 - 힙의 그 객체는 두 스레드에게 같은 물리 메모리다. 경쟁 상태(race condition)와 그걸 막는 락은 concurrency 글이 다뤘으니, 여기선 “공유되는 이유가 주소공간 공유다”만 쥐고 간다.

스레드는 프로세스보다 싸다

스레드를 새로 만드는 건 프로세스를 만드는 것보다 훨씬 싸다. 집을 새로 짓지 않고 사람만 하나 더 들이기 때문이다 - 주소공간·자원을 새로 만들 필요 없이, 자기 스택과 실행 상태만 갖추면 된다.

그래서 서버는 요청마다 프로세스가 아니라 스레드를 쓴다. 같은 프로세스 안에서 데이터를 공유하며 가볍게 여럿을 돌릴 수 있으니까.

그래도 공짜는 아니다

싸다고 무료는 아니다. 스레드마다 자기 스택을 위한 메모리가 예약되고(보통 수백 KB에서 수 MB), OS가 그 스레드를 스케줄링 대상으로 등록해 관리해야 한다. 스레드가 만 개면 스택 메모리만 수 기가바이트고, OS가 챙길 대상도 만 개다.

thread-pool 글이 “스레드는 만드는 것도 무제한도 비싸다”고 한 게 이것이다 - “비싸다”의 정체는 스택 메모리 + OS의 관리 부담이다. 그래서 무한정 만들 수 없고, 미리 만들어 재사용하는 풀이 나온다.

왜 무제한이 안 되나

스레드를 계속 늘리면 두 벽에 부딪힌다.

  • 메모리 벽 - 스레드마다 스택이 예약되니, 스레드 수 × 스택 크기만큼 메모리가 먹힌다. 너무 많으면 메모리가 바닥난다.
  • 스케줄링 벽 - CPU는 몇 개뿐인데 스레드가 수천이면, OS가 이들을 번갈아 태우느라 바꿔 태우는 일 자체에 시간을 쓴다. 정작 일할 시간이 줄어든다.

두 번째 벽, 즉 “CPU 하나로 많은 스레드를 번갈아 태우는 비용”이 다음 글 컨텍스트 스위치다. thread-pool 크기를 왜 그쯤으로 정하는지가 거기서 풀린다.

실무에서: 멈춘 스레드를 들여다본다

  • 스레드 덤프 - “지금 이 프로세스의 모든 스레드가 어느 코드에서 멈춰 있나”를 찍은 것. 서버가 굳었을 때, 스레드들이 다 같은 락이나 DB 응답을 기다리며 멈춰 있는지를 여기서 본다.
  • 스택 크기는 대개 안 건드린다 - 스레드 스택을 키우면 스레드 하나가 더 무거워져 만들 수 있는 수가 준다. 깊은 재귀 때문이라면 스택을 키우기보다 코드를 고친다.
  • 스레드 이름을 붙인다 - 풀의 스레드에 뜻 있는 이름을 주면, 덤프·로그에서 “어느 일을 하던 스레드가 멈췄나”가 바로 읽힌다.

정리

  • 스레드는 한 프로세스 안의 가벼운 실행 흐름 - 같은 집(주소공간)을 공유하되 자기 스택만 따로 갖는다.
  • 힙·자원은 공유, 스택·실행 상태는 따로 - 그래서 데이터를 쉽게 나누지만 공유 상태 문제가 생긴다(공유되는 이유가 주소공간 공유다).
  • 집을 새로 안 지어 프로세스보다 싸지만, 스택 메모리와 OS 관리 부담 때문에 무제한은 아니다 - 그래서 풀로 재사용한다.
  • 늘리면 메모리 벽과 스케줄링 벽에 부딪힌다.

다음 글은 그 스케줄링 벽 - CPU 하나로 여럿을 번갈아 돌리는 컨텍스트 스위치로 간다.

관련 글