← SEARCHABLE ARCHIVE
Operating System · 2026-07-15

스레드 동기화와 경쟁 상태

공유 데이터는 왜 실행할 때마다 결과가 달라질까?

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

같은 프로세스의 스레드는 힙과 전역 데이터를 공유한다. 덕분에 데이터를 빠르게 주고받을 수 있지만, 같은 값을 동시에 읽고 쓰면 실행할 때마다 결과가 달라질 수 있다.

이 문제를 Race Condition(경쟁 상태)이라고 부른다.

count++는 한 번의 작업이 아니다

int count = 0;

void AddCount()
{
    ++count;
}

코드에서는 한 줄이지만 CPU 입장에서는 값을 읽고, 1을 더하고, 다시 저장하는 여러 단계가 될 수 있다.

Thread A가 count 0을 읽음
Thread B가 count 0을 읽음
Thread A가 1을 저장함
Thread B가 1을 저장함

두 번 증가시켰지만 최종 결과는 2가 아니라 1이 된다. 스레드의 실행 순서는 운영체제 상황에 따라 달라지므로 테스트할 때 가끔만 나타나는 버그가 될 수도 있다.

Mutex로 한 번에 한 스레드만 들어가게 한다

std::mutex countMutex;
int count = 0;

void AddCount()
{
    std::lock_guard<std::mutex> lock(countMutex);
    ++count;
}

Mutex(뮤텍스)를 잠근 스레드가 임계 구역을 실행하는 동안 다른 스레드는 기다린다. lock_guard를 사용하면 함수가 끝나거나 예외가 발생해도 자동으로 잠금을 해제한다.

Atomic은 단순한 값에 사용할 수 있다

정수 하나를 증가시키는 것처럼 작은 연산은 Atomic(원자적 연산)을 사용할 수 있다.

std::atomic<int> count = 0;

void AddCount()
{
    ++count;
}

하지만 여러 변수의 관계를 한꺼번에 안전하게 유지해야 한다면 atomic 하나로 해결되지 않는다. 예를 들어 골드 차감과 아이템 추가가 반드시 같이 성공해야 한다면 둘을 하나의 임계 구역으로 보호해야 한다.

Deadlock도 주의해야 한다

두 스레드가 서로 상대방이 가진 락을 기다리면 아무도 진행하지 못한다. 이를 Deadlock(교착 상태)이라고 한다.

Thread A: Lock 1을 잡고 Lock 2를 기다림
Thread B: Lock 2를 잡고 Lock 1을 기다림

락을 항상 같은 순서로 획득하고, 잠금을 잡은 채 오래 걸리는 작업을 하지 않는 습관이 필요하다.

멀티스레딩의 핵심은 스레드를 많이 만드는 것이 아니다. 공유 데이터를 줄이고, 꼭 공유해야 하는 범위를 명확히 정하고, 그 범위에 맞는 동기화 도구를 사용하는 것이다.