한양과학기술고등학교

에듀테크 프로젝트 대시보드

경쟁 상태와 동기화

경쟁 상태와 동기화(Race Condition & Synchronization)

두 스레드가 같은 변수를 동시에 고치면, 코드는 멀쩡해 보여도 결과가 매번 달라진다. 왜 그런지, 그리고 임계 구역과 락(뮤텍스)이 어떻게 막는지 눈으로 보자.

1. count++ 는 한 번에 끝나지 않는다

고급 언어의 count = count + 1 한 줄은 CPU 입장에서는 최소 세 단계다.

// count++ 가 실제로 하는 일
LOAD  reg ← 메모리에서 count 값을 레지스터로 읽기
INC   reg ← 레지스터 값에 1 더하기
STORE reg → 레지스터 값을 메모리의 count 에 쓰기

운영체제는 아무 단계 사이에서나 스레드를 멈추고 다른 스레드로 전환(context switch)할 수 있다. 두 스레드가 거의 동시에 LOAD 하면, 둘 다 같은 옛날 값을 읽고, 각자 1을 더해, 각자 써넣는다 — 두 번 더했는데 1만 증가한다. 이렇게 사라진 갱신을 잃어버린 갱신(lost update)이라 한다.

2. 직접 실행해 보기

스레드 A와 B가 각각 공유 변수 count 를 5번씩 증가시킨다. 기대값은 10. 한 단계씩 실행하며 어느 스레드가 어느 명령을 수행하는지 골라 보고, 자동 실행으로 무작위 스케줄을 반복 관찰해 보자.

Thread A

regA = –
공유 count
0
🔓 lock: free

Thread B

regB = –
최종 count
–
기대값
10
잃어버린 갱신
0

3. 임계 구역과 상호 배제

문제가 생기는 LOAD → INC → STORE 구간을 임계 구역(critical section)이라 부른다. 규칙은 하나 — 임계 구역 안에는 한 번에 스레드 하나만. 이걸 상호 배제(mutual exclusion)라 하고, 이를 강제하는 도구가 락이다.

lock.acquire()      # 이미 잡혀 있으면 여기서 대기(block)
count = count + 1   # ← 임계 구역: 나 혼자만 있음이 보장됨
lock.release()      # 대기하던 다른 스레드를 깨움

위 데모에서 "락으로 보호"를 켜면, 한 스레드가 LOAD 하는 순간 락을 쥐고, STORE 를 마칠 때까지 다른 스레드는 blocked 상태로 얼어붙는다. 어떤 순서로 실행하든 결과는 항상 10.

도구핵심
Mutex (뮤텍스)0/1 상태의 락. "잠금을 건 스레드"만 풀 수 있음. 임계 구역 보호에 사용.
Semaphore (세마포어)정수 카운터. 자원 N개를 N개 스레드까지 동시 허용. P(감소·대기)/V(증가·신호).
Atomic 연산CPU가 LOAD+INC+STORE를 쪼개지지 않는 한 명령으로 실행 (CAS 등). 락 없이도 안전.
대가 — 락은 안전하지만 공짜가 아니다. 대기가 생기면 병렬성이 줄고, 락을 잘못 짜면 서로 상대의 락을 기다리는 교착 상태(deadlock)에 빠진다.

4. 경쟁 상태가 숨어 있는 실제 상황

  • 웹 서버가 재고 수량을 확인하고 감소시키는 사이에 다른 요청이 끼어들어 재고보다 많이 판매(oversell).
  • 조회수·좋아요 카운터를 여러 서버가 동시에 +1 해서 숫자가 조금씩 샌다.
  • "파일이 없으면 만든다" 를 두 프로세스가 동시에 검사·생성 (TOCTOU 취약점).
  • 은행 계좌 이체에서 잔액 확인과 출금 사이의 틈.

해결의 위치는 다양하다 — 코드에서는 뮤텍스/원자적 연산, 데이터베이스에서는 트랜잭션과 행 잠금, 분산 환경에서는 분산 락이나 단일 소유자로 직렬화한다.

한 줄 요약 — 여러 스레드가 공유 데이터를 읽고-고치고-쓰는 구간에서는 실행 순서에 따라 결과가 바뀐다. 그 구간을 임계 구역으로 지정하고 락으로 한 명씩만 들여보내면 결과가 항상 같아진다.