Skip to content

경쟁 상태 (Race Condition) ​

경쟁 상태는 여러 프로세스나 스레드가 같은 공유 자원에 동시에 접근할 때, 누가 먼저 실행되느냐에 따라 결과가 달라지는 상태입니다. 동시에 접근하는 순서가 꼬이면 데이터의 일관성이 깨집니다.

한눈에 보기 ​

  • 비유: 통장 하나를 두 사람이 동시에 씁니다. 잔액이 1만 원일 때 A와 B가 거의 동시에 잔액을 확인하고, 각자 "1만 원 + 내 입금 5천 원 = 1만 5천 원"이라고 계산해서 적습니다. 두 사람이 5천 원씩 넣었으니 2만 원이어야 하는데, 장부에는 1만 5천 원만 남습니다. 한 사람의 입금이 통째로 사라진 것입니다.
  • 문제는 "읽기 → 계산 → 쓰기"가 한 번에 끝나는 동작이 아니라 여러 단계로 나뉘어 있다는 점입니다. 그 사이에 다른 실행 흐름이 끼어들 수 있습니다.
  • 데이터베이스에서도 같은 일이 생기는데, 이것을 갱신 손실(Lost Update) 이라고 부릅니다. 동시성 문제와 격리 수준에서 함께 볼 수 있습니다.

코드로 보는 경쟁 상태 ​

스레드 4개가 공유 변수 balance를 각각 1,000번씩 1 늘립니다. 정상이라면 4,000이 나와야 합니다.

python
import threading
import time

balance = 0  # 여러 스레드가 함께 쓰는 공유 변수

def deposit(times):
    global balance
    for _ in range(times):
        current = balance      # 1) 읽기
        time.sleep(0)          # 이 틈에 다른 스레드로 넘어가기 쉽게 만듦
        balance = current + 1  # 2) 계산하고 쓰기

threads = [threading.Thread(target=deposit, args=(1000,)) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("기대값: 4000")
print("실제값:", balance)

실제로 세 번 실행한 결과입니다.

text
기대값: 4000
실제값: 1001
기대값: 4000
실제값: 1049
기대값: 4000
실제값: 1037
  • 실행할 때마다 값이 달라집니다. 스레드가 언제 바뀌는지는 운영체제가 정하기 때문에, 결과를 예측할 수 없다는 것 자체가 경쟁 상태의 특징입니다. 여러분이 실행하면 다른 숫자가 나올 수 있습니다.
  • time.sleep(0)은 "읽기"와 "쓰기" 사이에 다른 스레드가 끼어들 틈을 일부러 크게 만든 장치입니다. 이 줄이 없어도 경쟁 상태는 생길 수 있지만, 아주 드물게 나타나서 눈으로 확인하기 어렵습니다. 실제 버그가 "가끔만" 터져서 찾기 어려운 이유가 바로 이것입니다.

어떻게 값이 사라지는지 순서대로 보면 이렇습니다.

순서스레드 A스레드 Bbalance
1balance를 읽음 (100)100
2(잠깐 멈춤)balance를 읽음 (100)100
3101을 씀101
4101을 씀101

두 스레드가 한 번씩 더했으니 102가 되어야 하는데, A가 예전에 읽어 둔 100을 기준으로 덮어써서 B의 작업이 사라졌습니다.

임계 구역 (Critical Section) ​

임계 구역은 여러 실행 흐름이 공유 데이터를 건드리는 코드 부분입니다. 위 예제에서는 current = balance부터 balance = current + 1까지가 임계 구역입니다.

아래 그림에서 두 프로세스는 따로 실행되다가, 빨간 점(공유 데이터를 건드리는 부분)에서 길이 갈립니다. 이 지점이 임계 구역이고, 둘이 동시에 들어가지 못하게 막아야 합니다.

두 프로세스의 실행 흐름이 같은 임계 구역(빨간 점)을 거쳐 가는 모습

그림 출처: Wikimedia Commons (Kartikharia, CC BY-SA 4.0)

경쟁 상태를 막으려면 한 번에 하나만 임계 구역을 실행하도록 해야 합니다. 이 문제를 임계 구역 문제라고 하고, 올바른 해결책은 다음 세 가지 조건을 모두 만족해야 합니다.

조건뜻비유 (화장실 한 칸)
상호 배제 (Mutual Exclusion)한 프로세스가 임계 구역에 있으면 다른 프로세스는 들어갈 수 없음누가 안에 있으면 다른 사람은 못 들어감
진행 (Progress)임계 구역이 비어 있고 들어가려는 프로세스가 있으면, 그중 누가 들어갈지 무한정 미루지 않고 결정해야 함화장실이 비었는데 아무도 못 들어가고 서로 눈치만 보면 안 됨
한정 대기 (Bounded Waiting)한 번 들어가겠다고 요청한 프로세스는 언젠가는 들어갈 수 있어야 함 (다른 프로세스가 끼어드는 횟수에 한계가 있음)계속 새치기를 당해서 영원히 못 들어가는 사람이 없어야 함

이 조건을 지키는 도구가 락, 세마포어와 뮤텍스입니다. 위 예제를 뮤텍스로 고친 코드도 그 글에 있습니다.

운영체제 안에서 경쟁 상태가 생기는 경우 ​

경쟁 상태는 사용자 프로그램뿐 아니라 운영체제 커널 안에서도 생깁니다. 커널의 데이터(예: 프로세스 목록, 메모리 사용 현황)는 모든 프로세스가 함께 쓰는 공유 자원이기 때문입니다. 대표적인 경우가 세 가지 있습니다.

1. 커널 작업 도중 인터럽트가 발생할 때 ​

  • 문제: 커널 모드에서 어떤 데이터를 읽어 작업하던 중에 인터럽트가 들어오면, 인터럽트를 처리하는 코드가 같은 데이터를 고칠 수 있습니다. 인터럽트 처리가 끝나고 원래 작업으로 돌아오면, 예전에 읽어 둔 값으로 덮어써서 인터럽트 쪽의 변경이 사라집니다.
  • 해결: 커널이 중요한 데이터를 고치는 동안에는 인터럽트를 잠시 꺼서(disable) 끼어들 수 없게 합니다. 작업이 끝나면 다시 켭니다.
text
커널: count 읽기 (10)
        ── 인터럽트 발생 ──▶ 인터럽트 처리: count = 11
커널: count + 1 = 11을 씀     ← 인터럽트 쪽 변경이 사라짐 (12여야 함)

2. 시스템 콜로 커널 모드에 들어간 프로세스가 도중에 교체될 때 ​

  • 문제: 프로세스 1이 시스템 콜을 호출해서 커널 모드에서 데이터를 고치는 중에, 할당 시간이 끝나 CPU가 프로세스 2로 넘어갑니다(컨텍스트 스위칭). 프로세스 2도 시스템 콜로 같은 커널 데이터를 고칩니다. 다시 프로세스 1로 돌아오면 예전 값으로 덮어써서 프로세스 2의 작업이 반영되지 않습니다.
  • 해결: 프로세스가 커널 모드에서 작업하는 동안에는 할당 시간이 끝나도 CPU를 빼앗지 않고, 커널 모드에서 사용자 모드로 돌아갈 때 교체합니다. 이런 커널을 비선점형 커널이라고 합니다. 다만 이 방식은 반응 속도가 느려질 수 있어서, 리눅스 같은 요즘 커널은 커널 모드에서도 교체를 허용하고 대신 공유 데이터마다 락을 거는 쪽을 주로 씁니다.

3. 멀티 프로세서에서 공유 메모리의 커널 데이터에 접근할 때 ​

  • 문제: CPU가 여러 개면 두 CPU가 정말로 같은 순간에 커널 안의 같은 데이터를 고칠 수 있습니다. 이때는 인터럽트를 끄는 방법이 통하지 않습니다. 인터럽트를 꺼도 그 CPU 하나만 멈출 뿐, 다른 CPU는 계속 돌아가기 때문입니다.
  • 해결: 커널 안의 공유 데이터에 접근할 때마다 그 데이터에 대한 lock / unlock 을 합니다. 락을 쥔 CPU만 데이터를 고칠 수 있고, 다른 CPU는 락이 풀릴 때까지 기다립니다.

세 경우 정리 ​

상황끼어드는 쪽해결 방법
커널 작업 중 인터럽트인터럽트 처리 코드작업 동안 인터럽트 끄기
시스템 콜 처리 중 컨텍스트 스위칭다른 프로세스커널 모드에서는 CPU를 빼앗지 않기 (또는 락)
멀티 프로세서다른 CPU공유 데이터마다 lock / unlock

경쟁 상태와 데드락의 관계 ​

경쟁 상태를 막으려고 락을 쓰다 보면, 이번에는 락끼리 서로 기다리는 데드락이 생길 수 있습니다. 락은 "필요한 곳에만, 짧게, 항상 같은 순서로" 거는 것이 기본입니다.

아래 그림이 대표적인 경우입니다. Process 1은 Resource 1에 락을 건 채 Resource 2를 기다리고, Process 2는 Resource 2에 락을 건 채 Resource 1을 기다립니다. 둘 다 상대가 락을 풀기를 기다리니 영원히 진행하지 못합니다. 두 프로세스가 락을 같은 순서(Resource 1 → Resource 2)로 걸었다면 이런 일은 생기지 않습니다.

두 프로세스가 자원을 하나씩 잡고 상대의 자원을 기다리는 데드락 상황

그림 출처: gyoogle 기술 면접 정리, DeadLock

정리 ​

개념비전공자 버전 요약
경쟁 상태여럿이 같은 데이터를 동시에 고치다가 순서가 꼬여 값이 틀어지는 것
원인"읽기 → 계산 → 쓰기" 사이에 다른 실행 흐름이 끼어듦
특징실행할 때마다 결과가 달라서 재현하기 어려움
임계 구역공유 데이터를 건드리는 코드 부분. 한 번에 하나만 들어가야 함
세 가지 조건상호 배제, 진행, 한정 대기
커널 속 경쟁 상태인터럽트, 컨텍스트 스위칭, 여러 CPU 때문에 생김
해결 도구인터럽트 끄기, 락, 세마포어, 뮤텍스