한눈에 비교하기
프로세스는 실행 중인 프로그램의 인스턴스이며 운영체제가 자원과 주소 공간을 관리하는 단위입니다. 스레드는 그 프로세스 안에서 명령을 실행하는 흐름입니다. 하나의 프로세스는 적어도 하나의 메인 스레드로 실행됩니다.
| 구분 | 멀티프로세스 | 멀티스레드 |
| 메모리 공간 | 프로세스마다 격리 | 같은 프로세스의 주소 공간 공유 |
| 데이터 교환 | IPC가 필요 | 공유 데이터에 직접 접근 가능 |
| 생성,전환 비용 | 일반적으로 더 큼 | 일반적으로 더 작음 |
| 장애 격리 | 한 프로세스의 오류를 격리하기 쉬움 | 치명적인 오류가 프로세스 전체에 영향 |
| 주요 설계 문제 | IPC와 데이터 직렬화 | 데이터 레이스와 교착 상태 |
핵심은 메모리 공유 여부입니다. 격리는 안정성을 높이지만 통신 비용을 만들고, 공유는 빠른 협업을 가능하게 하지만 올바른 동기화를 요구합니다.
프로세스와 IPC
각 프로세스는 자신의 가상 주소 공간을 가집니다. 보통 코드, 전역·정적 데이터, 힙, 스택 같은 영역으로 나누어 설명하며, 한 프로세스의 일반 포인터로 다른 프로세스의 변수를 직접 읽거나 쓸 수 없습니다.
Process A Process B
├── Code ├── Code
├── Data ├── Data
├── Heap ├── Heap
└── Stack └── Stack
직접 접근 불가
서로 다른 프로세스가 데이터를 주고받으려면 운영체제가 제공하는 프로세스 간 통신(IPC, Inter-Process Communication)을 사용합니다. 파이프, 소켓, 메시지 큐, 공유 메모리, 파일 등이 대표적입니다. 방식에 따라 데이터 복사, 직렬화, 시스템 호출과 동기화 비용이 달라집니다.
대신 주소 공간이 분리되어 한 프로세스의 충돌이나 잘못된 메모리 접근이 다른 프로세스에 바로 전파될 가능성이 낮습니다. 브라우저가 탭이나 렌더러를 별도 프로세스로 나누고, 서버가 여러 워커 프로세스를 두는 이유 중 하나가 이런 장애 격리입니다.
스레드와 공유 메모리
같은 프로세스의 스레드는 코드, 전역·정적 데이터와 힙, 열린 파일 같은 프로세스 자원을 공유합니다. 반면 각 스레드는 독립적인 스택, CPU 레지스터 상태, 프로그램 카운터와 실행 상태를 가집니다.
Process
├── Shared: Code, Data, Heap
├── Thread 1
│ └── Stack, Registers, Program Counter
├── Thread 2
│ └── Stack, Registers, Program Counter
└── Thread 3
└── Stack, Registers, Program Counter
공유 힙의 객체나 전역 변수에 곧바로 접근할 수 있어 별도의 IPC 없이 빠르게 데이터를 교환할 수 있습니다. 하지만 접근할 수 있다는 사실이 동시에 접근해도 안전하다는 뜻은 아닙니다. 공유 데이터를 수정할 때는 소유권과 접근 순서를 설계해야 합니다.
생성과 문맥 교환 비용
스레드는 기존 프로세스의 주소 공간과 자원을 재사용하므로 일반적으로 새 프로세스보다 생성 비용과 메모리 사용량이 작습니다. 실행 대상을 바꾸는 문맥 교환에서도 현재 레지스터와 실행 상태를 저장하고 다음 작업의 상태를 복구해야 하지만, 프로세스 전환은 주소 공간과 관련된 상태까지 바뀔 수 있어 보통 더 무겁습니다.
Thread A 실행
↓ 현재 상태 저장
Thread B 상태 복구
↓
Thread B 실행
다만 실제 비용은 운영체제, 하드웨어, 캐시 상태와 작업 특성에 따라 달라집니다. 스레드가 항상 압도적으로 빠르다고 단정하기보다 측정 결과와 장애 격리, 통신 구조를 함께 판단해야 합니다. 작업을 지나치게 잘게 나누면 스케줄링과 동기화 비용이 실제 작업 시간보다 커질 수도 있습니다.
데이터 레이스와 동기화
여러 스레드가 적절한 동기화 없이 같은 메모리에 접근하고 그중 하나 이상이 값을 쓰면 데이터 레이스가 발생할 수 있습니다. C++에서 이런 데이터 레이스는 정의되지 않은 동작이므로 단순히 계산 결과 하나가 틀리는 데서 끝난다고 보장할 수 없습니다.
int counter = 0;
// 읽기, 증가, 저장이 하나의 원자적 연산은 아니다.
++counter;
두 스레드가 모두 0을 읽고 각각 1을 저장하면 두 번 증가시켰는데도 최종값이 1이 되는 갱신 손실이 나타날 수 있습니다. 공유 가변 상태를 줄이는 것이 첫 번째 방법이고, 공유가 필요하다면 목적에 맞는 동기화 도구를 사용해야 합니다.
- Mutex: 임계 구역에 한 번에 하나의 스레드만 진입하게 합니다.
- Atomic: 단순한 값의 읽기·쓰기·증가 등을 원자적으로 수행합니다.
- Semaphore: 동시에 자원을 사용할 수 있는 작업 수를 제한합니다.
- Condition Variable: 특정 조건이 충족될 때까지 스레드를 효율적으로 대기시킵니다.
#include <mutex>
int counter = 0;
std::mutex counterMutex;
void Increase()
{
std::lock_guard<std::mutex> lock(counterMutex);
++counter;
}
std::lock_guard는 범위를 벗어날 때 자동으로 뮤텍스를 해제합니다. 예외나 조기 반환이 있어도 잠금 해제를 빠뜨리지 않는 RAII 방식이므로 수동 lock()과 unlock() 호출보다 안전합니다.
교착 상태
동기화 도구 자체가 올바른 실행을 자동으로 보장하지는 않습니다. 두 스레드가 서로 상대방이 보유한 잠금을 기다리면 어느 쪽도 진행하지 못하는 교착 상태(Deadlock)가 생깁니다.
Thread A: Lock 1 획득 → Lock 2 대기
Thread B: Lock 2 획득 → Lock 1 대기
여러 잠금이 필요하다면 모든 코드에서 잠금 획득 순서를 통일하고, 잠금 범위를 작게 유지해야 합니다. C++에서는 여러 뮤텍스를 함께 획득해야 할 때 std::scoped_lock 같은 도구도 검토할 수 있습니다. 긴 I/O나 다른 스레드의 완료를 기다리는 동안 잠금을 계속 보유하지 않는지도 확인해야 합니다.
동시성과 병렬성
멀티스레드라고 해서 모든 스레드가 물리적으로 동시에 실행되는 것은 아닙니다. 동시성(Concurrency)은 여러 작업이 겹치는 시간 동안 진행되도록 구조화한 것이고, 병렬성(Parallelism)은 여러 CPU 코어에서 작업이 실제로 같은 순간에 실행되는 것입니다.
// 단일 코어: 빠르게 번갈아 실행
Core 1: Thread A → Thread B → Thread A
// 여러 코어: 실제 병렬 실행 가능
Core 1: Thread A
Core 2: Thread B
Core 3: Thread C
단일 코어에서도 한 작업이 I/O를 기다리는 동안 다른 작업을 진행해 동시성의 이점을 얻을 수 있습니다. 여러 코어가 있어도 공유 자원에 대한 경합이 심하거나 순차 실행 구간이 크면 기대한 만큼 빨라지지 않습니다.
작업 유형에 따른 선택
CPU-bound 작업
이미지 처리, 물리 계산, 압축, 암호화, AI 연산, 경로 탐색처럼 계산량이 많은 작업입니다. 서로 독립적인 단위로 나눌 수 있다면 여러 코어에서 멀티스레드나 멀티프로세스로 병렬 처리할 수 있습니다. 다만 분할, 결과 병합, 캐시 미스와 동기화 비용까지 포함해 실제 성능을 측정해야 합니다.
I/O-bound 작업
파일, 네트워크, 데이터베이스처럼 외부 응답을 기다리는 시간이 긴 작업입니다. 한 작업이 대기하는 동안 다른 작업을 처리할 수 있어 스레드나 비동기 I/O가 효과적입니다. 많은 연결을 다룰 때는 요청마다 스레드를 무제한 생성하기보다 스레드 풀이나 이벤트 기반 비동기 구조를 고려합니다.
- 멀티스레드
- 데이터 공유와 빠른 통신이 많고 하나의 프로그램 내부 기능을 병렬화할 때 적합합니다.
- 멀티프로세스
- 작업의 독립성과 장애 격리, 서로 다른 런타임이나 권한 경계가 중요할 때 적합합니다.
- 혼합 구조
- 여러 워커 프로세스를 띄우고 각 프로세스에서 스레드 풀을 사용하는 방식도 흔합니다.
게임 개발에서의 활용
하나의 게임 프로세스에서도 역할이 다른 여러 실행 흐름을 둘 수 있습니다.
Game Process
├── Game Thread
├── Render Thread
├── Audio Thread
├── Physics Worker Threads
└── Asset Loading Thread
게임 로직과 엔진 객체 상태는 주로 게임 스레드가 관리하고, 렌더링 준비, 물리 계산, 애셋 읽기처럼 분리 가능한 작업은 다른 스레드나 워커가 맡을 수 있습니다. 그러나 엔진 객체가 모두 스레드 안전한 것은 아닙니다. 예를 들어 언리얼 엔진의 많은 UObject 작업은 임의의 백그라운드 스레드에서 수행하면 안 됩니다.
- 백그라운드 작업은 엔진 객체를 직접 수정하지 않고 순수 계산이나 파일 읽기를 수행합니다.
- 계산 결과를 스레드 안전한 방식으로 게임 스레드에 전달합니다.
- 게임 스레드가 결과를 검증한 뒤 엔진 객체와 실제 게임 상태에 반영합니다.
멀티스레딩의 목표는 스레드 수를 늘리는 것이 아니라 프레임의 긴 작업을 분산하면서도 상태의 소유권을 명확하게 유지하는 것입니다. 공유 데이터, 작업 의존성, 완료 시점과 게임 스레드 복귀 경로를 먼저 설계해야 합니다.
정리
- 프로세스는 독립된 주소 공간을 가지며, 프로세스 간 데이터 교환에는 IPC가 필요하다.
- 같은 프로세스의 스레드는 코드·데이터·힙을 공유하지만 스택과 실행 상태는 개별적으로 가진다.
- 스레드는 일반적으로 생성과 문맥 교환이 가볍지만 공유 가변 상태 때문에 동기화가 필요하다.
- 동기화되지 않은 공유 메모리 접근은 데이터 레이스를 만들고, 잘못된 잠금 순서는 교착 상태를 만들 수 있다.
- 동시성은 여러 작업을 함께 진행하는 구조이고, 병렬성은 여러 코어에서 실제로 동시에 실행하는 것이다.
- 실무에서는 성능뿐 아니라 데이터 공유량, 장애 격리, 작업 크기와 유지보수 비용을 함께 고려한다.
멀티프로세스와 멀티스레드는 어느 한쪽이 항상 우수한 선택지가 아닙니다. 메모리를 공유할 것인지 격리할 것인지, 작업 사이에 어떤 데이터를 얼마나 자주 주고받는지를 먼저 정하면 적절한 구조와 동기화 전략을 고르기 쉬워집니다.
'운영체제' 카테고리의 다른 글
| [운영체제] 프로세스와 CPU 실행 구조 (0) | 2026.07.27 |
|---|