스레드
프로세스 내에서 실행되는 흐름의 최소 단위
— LWP(Lightweight Process)라고도 불리는 멀티스레딩의 근간
어제는 분명 잘 작동하던 코드가 오늘 디버거를 붙이니까 아무 일도 없었다는 듯 멀쩡히 돌아간다. 데드락에 걸려 굳어버린 내 프로그램과 내 표정은 정확히 1:1 대칭을 이룬다.
1. 개요
스레드는 프로세스가 할당받은 자원의 바운더리 안에서 숟가락을 얹어 작동하는 실행 단위이다. 과거에는 무거운 프로세스를 여러 개 띄워 멀티태스킹을 시도했으나, 막대한 컨텍스트 스위칭 비용 감당이 안 되어 탄생한 구원투수다. 프로세스의 자원을 공유한다는 매혹적인 특징 덕분에 몸무게가 매우 가볍고 통신 속도가 기가 막히게 빠르다. 그러나 자원을 공유한다는 이 극강의 장점은, 곧바로 공유 자원을 지키기 위한 동기화 문제라는 무시무시한 부메랑이 되어 개발자들의 목덜미를 낚아챈다.(...)
2. 한 지붕 아래 독방 쓰기: 스레드의 자원 공유 방식
스레드는 소속된 프로세스의 코드, 데이터, 힙 영역을 사이좋게 공유하지만, 독자적인 명령 수행 기록을 보존해야 하므로 오직 자신만의 스택 영역과 레지스터 세트만 독립적으로 분배받는다. 힙 영역을 통째로 공유하는 덕택에 귀찮은 IPC 없이 그냥 전역 변수나 동적 객체를 슥 읽고 쓰면 장땡이라 성능상 이점이 엄청나다. 하지만 동기화 처리 없이 여러 스레드가 동시에 하나의 값에 손을 대는 순간 경쟁 상태(Race Condition)1가 발생하여 데이터가 걸레짝이 되기 십상이다. 이를 막고자 자물쇠(뮤텍스나 세마포어)를 덕지덕지 채우다 보면, 서로가 가진 자물쇠 열쇠만을 하염없이 기다리다 시스템 전체가 돌처럼 굳어버리는 교착 상태(Deadlock)라는 파멸을 맞이한다.
3. 멀티스레딩의 악몽: 하이젠버그와의 전쟁
물 흐르듯 차례차례 돌던 싱글스레드 프로그램과 달리, 멀티스레드 프로그램은 CPU 스케줄러가 어느 스레드를 먼저 가동할지 감히 인간계에서 예측할 수 없다. 로컬 환경에서 10,000번 테스트할 때는 멀쩡하다가, 하필이면 실 서비스 배포 당일에 귀신같이 에러가 터져 나오는 기괴한 버그들을 겪게 되는데, 물리학의 불확정성 원리마냥 관찰하려 들면 사라지는 이 악마 같은 버그들을 하이젠버그(Heisenbug)2라고 부른다. 이 디버깅의 악랄함 때문에 자바나 C++ 개발자들은 밤마다 눈물을 흘리며, 자바스크립트 기반의 Node.js 진영처럼 아예 싱글스레드 이벤트 루프 모델로 도망쳐 '동기화 걱정 없는 유토피아'를 외치는 이들도 존재한다. 물론 이쪽도 CPU 헤비한 작업 하나 돌렸다가 서버 전체가 깡통처럼 굳어버리는 역풍을 맞지만 말이다.(...)
4. 관련 밈 및 드립
4.1. 동시성은 병렬성이 아니다(Concurrency is not Parallelism)
Go 언어의 창시자 중 한 명인 롭 파이크(Rob Pike)의 명발표 타이틀이자 업계 불후의 밈. 싱글 코어에서 여러 스레드를 존나게 빨리 교대 근무 시켜서 동시에 도는 것처럼 '착각'을 일으키는 동시성(Concurrency)과, 실제 멀티 코어 하드웨어에서 물리적으로 두 개 이상의 작업이 '진짜 동시에' 수행되는 병렬성(Parallelism)을 혼동하여 똥코드를 양산하는 초보 개발자들의 뚝배기를 무자비하게 깨트릴 때 소환되는 주문이다.
4.2. 식사하는 철학자들(Dining Philosophers Problem)
다섯 명의 철학자가 둥근 식탁에 앉아, 식사를 하기 위해 양옆에 놓인 두 개의 포크(자물쇠)를 모두 쥐어야 하는 가상의 교착 상태 비유 모델. 모두가 동시에 왼쪽 포크를 집어 드는 순간, 아무도 오른쪽 포크를 쥐지 못해 영원히 굶어 죽는 비극을 다룬다. 멀티스레드 환경에서 세련되지 못한 락(Lock) 분배 구조를 설계하면 시스템이 어떻게 정중하게 굶어 죽는지(Deadlock)를 가장 예술적으로 표현한 밈이다.
5. 여담
- 그린 스레드(Green Thread): OS의 물리적인 커널 스레드에 1:1로 매핑하지 않고, 가상머신이나 런타임 라이브러리(Java 초창기 시절이나 Go의 고루틴 등) 수준에서 가상으로 흉내 내어 스케줄링하는 가벼운 스레드를 일컫는다. 커널 영역으로 넘어가는 비용이 없어 비행기급 속도를 자랑한다.
- Thread-Safe에 대한 착각: 어떤 라이브러리가 API 문서에 'Thread-Safe'라고 명시해 놨더라도, 그것은 지들 내부 로직이 안 깨진다는 뜻일 뿐이다. 그걸 가져다 쓰는 니 로직이 엉망진창이면 여전히 데이터 정합성이 우주 저 멀리 안드로메다로 떠나버린다.(...)
- 스레드 풀(Thread Pool)의 정체: 스레드를 새로 만들고 죽이는 과정 역시 OS 입장에서는 상당히 허리가 휘는 무거운 작업이다. 그리하여 영리한 개발자들은 미리 일정 수의 스레드를 웅덩이(스레드 풀)에 담가두고, 필요할 때마다 꺼내 쓰고 반납하는 식의 노예 굴림 아키텍처를 기본 소양으로 삼고 있다.