프로세스는 독립된 주소 공간과 자원을 가진 실행 단위입니다. 스레드는 프로세스 안에서 동작하는 실행 흐름이라 stack 같은 일부 실행 상태는 따로 갖지만 code, heap, file descriptor 같은 자원은 공유합니다. 그래서 프로세스는 격리가 잘 되지만 생성과 전환 비용이 상대적으로 크고, 스레드는 가볍게 협업할 수 있지만 공유 데이터 경쟁을 조심해야 합니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
스레드마다 따로 가지는 것과 공유하는 것은 무엇인가요?
stack, register 상태, program counter는 스레드마다 따로 가집니다. 각 스레드가 독립적인 함수 호출 경로와 실행 위치를 가져야 하기 때문입니다.
반대로 code 영역, heap, file descriptor, signal handler는 같은 프로세스 안 스레드들이 공유합니다. 전역 변수나 힙 객체를 여러 스레드가 동시에 참조할 수 있는 이유입니다.
PCB와 TCB는 무엇인가요?
OS가 프로세스 실행 상태를 저장하는 자료구조가 PCB(Process Control Block)입니다. 프로세스 ID, 주소 공간 정보, file descriptor 테이블, 레지스터 스냅샷, 프로세스 상태(running/ready/blocked)를 담습니다. context switching 시 OS가 현재 PCB를 저장하고 다음 프로세스의 PCB를 불러옵니다.
스레드도 유사하게 TCB(Thread Control Block)에 스레드 ID, stack 포인터, 레지스터 상태, program counter를 담습니다. 스레드 전환은 주소 공간이 바뀌지 않아 PCB 전환보다 가볍습니다.
프로세스 간 통신은 왜 별도 기법이 필요한가요?
프로세스는 OS가 주소 공간을 분리해 관리하기 때문에 한 프로세스가 다른 프로세스의 메모리를 직접 접근할 수 없습니다.
그래서 OS가 제공하는 IPC(Inter-Process Communication) 메커니즘이 필요합니다. pipe는 단방향 바이트 스트림, socket은 네트워크를 포함한 양방향 통신, shared memory는 같은 물리 페이지를 두 프로세스에 매핑해 빠르게 공유하지만 별도 동기화가 필요합니다. 스레드 간 통신은 같은 heap을 공유하므로 전역 변수나 객체 참조로 직접 주고받을 수 있지만, 그만큼 race condition 위험이 따릅니다.
스레드를 쓰면 항상 성능이 좋아지나요?
아닙니다. CPU-bound 작업은 코어 수보다 많은 스레드를 만들면 오히려 느려질 수 있습니다. 실제 연산 시간보다 context switching과 cache 무효화 비용이 커지기 때문입니다.
lock 경합이 심한 경우도 마찬가지입니다. 공유 자원에 mutex가 걸려 있으면 스레드들이 순서대로 기다리게 되어 병렬성의 이점이 사라집니다. Amdahl의 법칙에 따르면 순차 실행이 필요한 코드 비율이 성능 향상의 상한을 결정합니다.
I/O-bound 작업에서는 스레드가 I/O를 기다리는 동안 다른 스레드가 CPU를 쓸 수 있어 효과적입니다.
스레드가 공유 메모리를 쓰면 어떤 위험이 생기나요?
두 스레드가 동시에 같은 변수를 읽고 쓰면 race condition이 발생합니다. 실행 순서에 따라 결과가 달라지고, 재현하기 어려운 버그로 이어집니다.
count = count + 1; // read → add → write, 세 단계로 실행됨
두 스레드가 이 코드를 동시에 실행하면 둘 다 같은 값을 읽고 각자 +1해서 쓰므로 한 번만 증가할 수 있습니다. 이를 막으려면 mutex로 critical section을 보호하거나, 하드웨어 지원의 atomic 연산을 쓰거나, 스레드 간 메시지 전달 방식으로 공유 상태 자체를 없애야 합니다.
Python의 GIL은 무엇이고 왜 문제가 되나요?
CPython에는 GIL(Global Interpreter Lock)이 있습니다. 한 시점에 하나의 스레드만 Python bytecode를 실행할 수 있게 막는 인터프리터 수준의 lock입니다.
GIL 때문에 멀티스레드를 써도 CPU-bound 작업은 실제 병렬 실행이 되지 않습니다. 그래서 Python에서 CPU-bound 병렬 처리는 multiprocessing 모듈로 프로세스를 여러 개 띄웁니다. 프로세스마다 별도의 인터프리터와 GIL이 생기기 때문입니다. 반면 I/O-bound 작업은 I/O 대기 중 GIL을 해제하므로 멀티스레드가 효과적입니다.
멀티프로세스가 유리한 상황은 언제인가요?
장애 격리가 중요하거나, 인터프리터 제약으로 스레드 간 병렬 실행이 막혀 있을 때 멀티프로세스가 유리합니다.
크롬은 탭마다 별도 프로세스를 씁니다. 한 탭의 렌더러가 크래시해도 다른 탭에 영향을 주지 않기 위해서입니다. 같은 프로세스 안 스레드라면 하나가 죽을 때 전체가 함께 종료됩니다. Node.js 클러스터 모드도 단일 이벤트 루프의 한계를 멀티코어에서 보완하기 위해 여러 프로세스를 사용합니다.
멀티스레드 대신 코루틴을 쓰는 이유는?
스레드는 OS 스케줄러가 선점적으로 전환합니다. 실행 중 강제로 멈추고 다른 스레드를 실행할 수 있어 context switching 비용이 있습니다.
코루틴은 협력적으로 전환됩니다. await나 yield 지점에서 코루틴 자신이 제어권을 넘겨야 전환이 일어납니다. OS 스케줄링 없이 하나의 스레드 안에서 동작하므로 전환 비용이 거의 없고, 수만 개의 동시 연결도 가볍게 처리할 수 있습니다.
JavaScript의 async/await, Python의 asyncio, Go의 goroutine이 이 범주에 속합니다. I/O-bound 작업이 많고 동시 연결 수가 많을 때 스레드보다 훨씬 적은 메모리로 동시성을 달성할 수 있습니다.
부가 설명
처음에는 둘 다 CPU에서 실행되는 단위라 비슷해 보입니다. 차이는 문제가 터지는 지점에서 선명해집니다. 한 프로세스가 죽어도 다른 프로세스의 메모리는 보통 직접 망가지지 않지만, 같은 프로세스 안의 스레드 하나가 공유 heap을 잘못 바꾸면 다른 스레드도 바로 영향을 받습니다.
차이는 메모리 공유와 격리, 생성 비용, 통신 방식에서 드러납니다. 프로세스 간 통신은 IPC가 필요하고, 스레드 간 통신은 공유 메모리로 쉽지만 lock 같은 동기화가 따라옵니다. 결국 프로세스는 격리, 스레드는 가벼운 병행 실행에 가깝습니다.
한 줄 정리
프로세스는 격리된 실행 단위이고, 스레드는 같은 프로세스 자원을 공유하는 실행 흐름입니다.