프로세스와 스레드
실행 인스턴스와 그 안의 흐름
프로세스는 프로그램의 실행 인스턴스로 격리된 메모리 공간을 가진다. 스레드는 한 프로세스 안에서 같은 메모리를 공유하며 동시에 흐르는 실행 경로이며, 빠른 통신이 장점인 대신 공유 변수를 동시에 수정할 때 race condition이 발생해 락 같은 동기화 장치가 필요해진다.
개념
프로그램은 디스크에 저장된 명령어 파일이고, 그걸 메모리에 올려서 CPU가 실제로 실행 중인 상태가 프로세스다. 같은 프로그램을 두 번 실행하면 프로세스는 두 개가 되고 각자 독립된 메모리 공간을 가진다. 스레드는 한 프로세스 안에서 코드를 "걸어가는 사람" 하나 — 지금 실행 중인 경로다. 스레드가 여러 개면 여러 사람이 같은 책장(메모리)을 보면서 서로 다른 페이지를 동시에 읽고 있는 그림이 된다.
프로그램 vs 프로세스
- 프로그램: 디스크의 실행 파일. 가만히 있는 바이트 덩어리
- 프로세스: 그 파일이 메모리에 올라와 CPU가 실행 중인 상태
크롬을 세 번 켜면 프로세스는 세 개다. 각자 PID, 메모리, 현재 실행 위치를 따로 가진다. 서로 남의 메모리는 못 본다.
프로세스가 소유하는 것
OS가 프로세스를 만들 때 딸려오는 세트:
- 메모리 공간 (코드, 데이터, 힙, 스택)
- Program Counter — 지금 CPU가 어느 명령어를 실행 중인지
- 레지스터 값들
- 열린 파일 목록 (file descriptor table)
- PID
핵심은 메모리 격리다. 한 프로세스는 다른 프로세스의 메모리를 못 본다. 크롬이 터져도 스포티파이가 멀쩡한 이유.
스레드는 실행 흐름 하나
한 프로세스 안에서 CPU가 "어디를 읽고 있는지" 가리키는 북마크는 꼭 하나일 필요가 없다. 여러 개 꽂아두고 CPU가 돌아가며 실행하면 스레드가 여러 개 있는 것이다.
스레드가 따로 가지는 것:
- Program Counter (자기가 어느 줄에 있는지)
- 레지스터 값
- 콜 스택 (여태 거쳐온 함수들)
스레드가 공유하는 것:
- 힙 메모리 (변수, 객체)
- 열린 파일
- 코드 자체
"같은 책 안을 걸어가는 사람 하나"가 스레드다. 책은 공유, 발자국은 따로.
왜 스레드는 가볍나
프로세스를 새로 만들 때 OS가 해야 할 일: 메모리 공간 할당, 페이지 테이블 구성, 파일 테이블 복사, PID 발급, 기타 자원 할당.
스레드를 새로 만들 때: 스택 하나 + 레지스터 세트만 추가. 메모리와 파일은 이미 있는 걸 그대로 쓴다.
할 일이 적으니 가볍다. 컨텍스트 스위칭도 같은 프로세스 안 스레드끼리는 주소 공간이 안 바뀌어서 더 싸다.
Race Condition
같은 메모리를 공유한다는 건 같은 변수에 동시에 쓸 수 있다는 뜻이고, 그게 버그의 씨앗이다.
count = 0
def 증가():
global count
count = count + 1
count = count + 1은 눈에는 한 줄이지만 CPU 단계로는 세 단계다.
- 메모리에서 count 읽기
- 1 더하기
- 메모리에 다시 쓰기
두 스레드가 동시에 이 함수를 호출하면 이렇게 엇갈릴 수 있다.
스레드 A: count 읽음 → 0
스레드 B: count 읽음 → 0 # A가 아직 안 썼음
스레드 B: 0 + 1 = 1 저장
스레드 A: 0 + 1 = 1 저장 # B가 쓴 걸 덮어씀
결과는 count == 1. 한 번의 증가가 사라졌다. Git에서 두 브랜치가 같은 줄을 고쳐 merge conflict가 나는 것과 본질이 같다.
악질인 점은 항상 발생하지 않는다는 것. 운이 맞으면 엇갈리지 않고 정상 동작하다가 가끔 틀린다. 재현이 안 되니 디버깅이 지옥이다.
락으로 막기
"한 번에 한 스레드만 이 구역에 들어오게" 막는 장치가 락이다.
lock = Lock()
def 증가():
global count
with lock:
count = count + 1
with lock: 안에 한 스레드가 들어가 있으면 다른 스레드는 밖에서 대기한다. 세 단계가 중간에 끊기지 않으니 race condition이 사라진다.
대신 대기하는 스레드는 노는 셈이라 성능 비용이 든다. 락을 많이 걸면 멀티스레드의 의미가 줄고, 안 걸면 버그. 이게 동시성 프로그래밍의 상시 트레이드오프다.
크롬 탭은 왜 프로세스로 띄우나
스레드로 구현해도 동작은 한다. 그런데 크롬은 일부러 탭마다 프로세스를 띄운다.
- 안정성: 한 탭이 터져도 다른 탭이 안 죽음. 메모리가 격리돼 있으니까
- 보안: 악성 사이트가 같은 주소 공간에 있으면 옆 탭의 비밀번호를 훔쳐볼 수 있다. 프로세스 격리로 OS가 막아준다
- 샌드박싱: 탭 단위로 권한을 떨어뜨리기 쉬워진다
"가벼움"을 버리고 "격리"를 산 셈. 트레이드오프는 항상 의식적으로 선택하는 것이다.
더 보기
- 컨텍스트-스위칭 — CPU가 스레드 사이를 어떻게 전환하나