멀티쓰레드의 동시성 모델과 동기화 설계

멀티쓰레드의 동시성·병렬성, 쓰레드 모델, 동기화와 데드락 방지 원칙을 정리한다.

2026-08-14 · 최초 발행 2026-01-03

하나의 프로세스 안에서 나뉘는 실행 흐름

멀티쓰레드(MultiThread)는 하나의 프로세스가 여러 쓰레드를 만들어 동시에 실행하는 방식이다. 동시성(Concurrency)은 여러 작업이 논리적으로 함께 진행되는 상태를 뜻하고, 병렬성(Parallelism)은 여러 작업이 물리적으로 동시에 실행되는 상태를 뜻한다.

단일 쓰레드는 하나의 실행 흐름을 순차적으로 처리한다. 구조는 단순하지만 I/O를 기다리는 동안 CPU가 유휴 상태가 될 수 있다. 멀티쓰레드는 여러 실행 흐름을 두어 한 쓰레드가 I/O를 기다릴 때 다른 쓰레드를 실행할 수 있으며, 멀티코어 CPU에서는 실제 병렬 실행도 가능하다.

멀티쓰레드프로세스Thread 1(UI)Thread 2(Network)Thread 3(File I/O)Thread 4(Compute)공유 메모리(Code, Data, Heap)독립 Stack독립 Stack독립 Stack독립 Stack

공유 자원으로 얻는 이점과 비용

UI 처리와 파일 로드를 같은 실행 흐름에 두면 파일을 읽는 동안 UI가 멈출 수 있다. 작업을 분리하면 UI는 입력을 계속 처리하고 파일 로드는 별도 쓰레드에서 진행된다.

단일 쓰레드: [UI] → [파일 로드] → [UI 멈춤] → [완료]
멀티 쓰레드: [UI 쓰레드] + [파일 로드 쓰레드] → [UI 계속 반응]

쓰레드는 Code, Data, Heap 영역과 파일 디스크립터를 공유한다. 프로세스 간 통신(IPC) 없이 메모리에 직접 접근할 수 있어 데이터 복사가 필요하지 않다.

구분 멀티 프로세스 멀티 쓰레드
메모리 공유 IPC 필요 직접 접근
통신 속도 느림 빠름
메모리 사용 많음 적음
데이터 복사 필요 불필요

쓰레드 생성 비용은 프로세스의 1/10 ~ 1/100이며, Context Switch는 프로세스가 수십 마이크로초인 데 비해 쓰레드는 수 마이크로초로 10~100배 빠르다. 메모리를 재사용하고 CPU 시간을 절약할 수 있어 시스템 부하도 줄일 수 있다.

멀티코어 환경에서는 각 쓰레드가 서로 다른 코어에서 실행될 수 있다. 이론적으로 N개 코어는 N배 성능으로 이어지지만, 실제 성능은 오버헤드를 고려해 0.7N ~ 0.9N배다.

단일 코어: Thread 1 → Thread 2 → Thread 3 (순차)
4 코어: Thread 1 + Thread 2 + Thread 3 + Thread 4 (병렬)

공유 자원이 만드는 동기화 문제

여러 쓰레드가 공유 자원에 동시에 접근하면 실행 순서에 따라 결과가 달라지는 Race Condition이 발생할 수 있다. 이 경우 데이터 일관성이 손상될 수 있으므로 Mutex나 Semaphore로 접근을 조정해야 한다. 대신 동기화 자체가 성능 오버헤드와 구현 복잡도를 만든다.

Deadlock은 둘 이상의 쓰레드가 서로가 보유한 자원을 기다리며 무한히 대기하는 상태다. 상호 배제, 보유 및 대기, 비선점, 순환 대기가 모두 성립할 때 발생할 수 있다.

Lock A 보유Lock B 보유Thread 1Lock B 대기Thread 2Lock A 대기Deadlock

쓰레드 실행 순서는 예측하기 어렵고, 재현하기 힘든 버그나 관찰하면 사라지는 Heisenbug로 이어질 수 있다. 생성·삭제, Context Switch, 동기화에는 관리 비용이 들며, 각 쓰레드에 Stack이 할당되므로 쓰레드 수가 많아질수록 메모리 부담도 커진다.

사용자 쓰레드와 커널 쓰레드의 연결 방식

Many-to-One 모델은 여러 사용자 쓰레드를 하나의 커널 쓰레드에 연결하고 사용자 수준에서 관리한다. 쓰레드 전환이 빠르고 시스템 콜이 필요 없지만, 멀티코어를 활용할 수 없으며 하나가 블록되면 전체가 블록된다. Java 초기의 Green Thread가 예시다.

One-to-One 모델은 사용자 쓰레드 하나를 커널 쓰레드 하나에 연결한다. 멀티코어 활용이 가능하고 한 쓰레드가 블록되어도 다른 쓰레드는 계속 실행할 수 있다. 생성 비용이 높고 쓰레드 수에 제한이 있다는 점이 있다. Windows 쓰레드와 Linux NPTL이 여기에 해당한다.

Many-to-Many 모델은 M개 사용자 쓰레드를 N개 커널 쓰레드에 연결하는 하이브리드 방식이다. 유연성과 멀티코어 활용이라는 장점이 있지만 구현과 관리가 복잡하다. Solaris 쓰레드(과거)와 Go Goroutine이 예시다.

생성과 동기화의 기본 형태

POSIX 쓰레드에서는 pthread_create로 쓰레드를 만들고 pthread_join으로 종료를 기다린다.

#include <pthread.h>

void* thread_function(void* arg) {
    printf("Thread running\n");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    pthread_join(thread, NULL);
    return 0;
}

Java에서는 Thread를 상속한 클래스의 인스턴스를 시작하고 종료를 기다릴 수 있다.

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running");
    }
}

MyThread t = new MyThread();
t.start();
t.join();

공유 자원의 임계 영역은 Mutex로 보호할 수 있다.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);
// Critical Section
pthread_mutex_unlock(&mutex);

Semaphore 역시 동기화에 사용할 수 있다.

sem_t sem;
sem_init(&sem, 0, 1);

sem_wait(&sem);
// Critical Section
sem_post(&sem);

쓰레드 풀은 미리 만든 쓰레드 집합을 작업 요청에 재사용하는 구조다. 생성과 삭제 비용을 줄이고 응답 시간을 빠르게 하며, 쓰레드 수를 제한해 시스템 과부하를 막는다.

[작업 큐] → [쓰레드 풀] → [작업 처리]
              ├ Thread 1
              ├ Thread 2
              ├ Thread 3
              └ Thread 4

서버부터 게임 엔진까지의 분리 방식

웹 서버는 메인 쓰레드가 연결을 수락하고 워커 쓰레드가 요청을 처리하는 방식으로 쓰레드 풀을 활용할 수 있다. 동시 다중 클라이언트 처리와 처리량, 응답성 향상에 연결된다.

웹 브라우저는 UI, 네트워크, 렌더링, JavaScript 실행을 분리한다. UI 반응을 유지하면서 백그라운드에서 리소스를 내려받고 페이지를 렌더링할 수 있다.

데이터베이스에서는 클라이언트 요청별 쓰레드 할당, 트랜잭션 병렬 처리, 인덱스 병렬 생성에 쓰일 수 있다. 높은 동시성과 빠른 응답 시간, 처리량 증가가 목적이다.

게임 엔진은 렌더링, 물리 시뮬레이션, 인공지능, 멀티플레이어 통신, 오디오 처리를 나눌 수 있다. 부드러운 프레임과 복잡한 시뮬레이션, 실시간 처리를 지원한다.

잠금 범위와 작업 특성으로 설계하기

쓰레드 안전성은 공유 상태를 줄이는 데서 시작한다. 상태가 바뀌지 않는 불변 객체는 동기화 없이 안전하게 공유할 수 있고, 지역 변수는 Stack에 저장되어 쓰레드마다 독립적이다. Lock 범위를 작게 유지하고 Lock-Free 자료구조나 원자적 연산(Atomic Operation)을 고려할 수 있다.

데드락을 피하려면 모든 쓰레드가 같은 순서로 Lock을 얻도록 정하고, Lock 획득에 제한 시간을 두어 실패 시 재시도하거나 포기하게 할 수 있다. Lock에 우선순위를 부여해 낮은 우선순위에서 높은 우선순위로 획득하는 계층 구조도 순환 대기를 막는 방법이다.

쓰레드 수는 CPU 코어 수와 작업 성격에 맞춰 조정한다. I/O 바운드 작업에는 많은 쓰레드를, CPU 바운드 작업에는 코어 수만큼의 쓰레드를 둔다. 작업은 독립적인 단위로 나누고 데이터 병렬성이나 파이프라인 처리를 활용할 수 있다. False Sharing을 피하고 데이터 지역성과 캐시 라인 정렬을 고려하는 일도 성능 설계에 포함된다.

멀티쓰레드운영체제동시성병렬성동기화