운영체제

[OS] 경쟁 상태(Race Condition)

샥쿠 2024. 5. 14. 19:11

경쟁 상태(Race Condition)

여러 프로세스(또는 스레드)가 병행(concurrent) 또는 병렬(parallel)하게 실행되며 공유 자원에 동시에 접근할 때 실행 순서에 따라 결과가 달라질 수 있는 현상이다.

하나의 예시로 경쟁 상태가 발생하여 공유 데이터의 무결성이 보장되지 않는 경우를 살펴보자. 다음은 두개의 스레드가 동시에 공유 데이터(counter) 값을 변경시키는 프로그램을 파이썬 코드로 작성한 것이다.

import threading

# 공유 자원
counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1

def decrement():
    global counter
    for _ in range(1000000):
        counter -= 1

# 두 개의 스레드 생성
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=decrement)

# 스레드 시작
thread1.start()
thread2.start()

# 두 스레드가 종료될 때까지 대기
thread1.join()
thread2.join()

print("Counter value:", counter)

이 코드에서는 counter라는 변수를 두 개의 함수 increment와 decrement에서 동시에 조작하고 있다. 이 코드를 실행하면 두 개의 스레드가 동시에 counter를 수정하려고 시도할 수 있다. 이 때 increment와 decrement 함수가 counter의 값을 동일한 횟수만큼 수정한다면 counter의 최종 결과 값은 0이 될 것으로 예측할 수 있으나, 실제로는 경쟁 상태의 발생으로 인해 실행할 때마다 다른 결과를 얻을 수 있다.

경쟁 상태가 발생하는 이유

counter += 1과 counter -= 1 같은 연산은 보통은 하나의 단계처럼 보이지만, 컴퓨터의 관점에서는 다음과 같은 세 가지 단계로 나뉜다.

  1. 메모리의 counter 값을 CPU 레지스터에 로드: CPU는 연산을 수행하기 전에 메모리에서 counter의 현재 값을 읽고 CPU 내부의 레지스터(Register)에 로드한다. CPU 레지스터에 로드한 후 연산을 수행함으로써 더 빠른 연산을 할 수 있다.
  2. 연산 수행: CPU는 레지스터에 로드한 counter의 값을 증가시키거나 감소시키는 연산을 수행한다. 예를 들어, counter += 1은 현재 counter의 값에 1을 더하는 연산을 수행하고, counter -= 1은 현재 counter의 값에서 1을 빼는 연산을 수행한다.
  3. 결과를 메모리의 counter에 저장하기: CPU는 연산 결과를 메모리에 다시 저장한다. 이 단계에서 메모리에 저장된 counter의 값이 변경된다.

이러한 세 단계는 보통 아주 빠르게 이루어지기 때문에 우리는 이를 하나의 단계로 인식하기 쉽지만, 세 단계가 한번에 완료되지 않고 중간에 컨텍스트 스위칭이 일어난다면 예상치 못한 결과가 발생한다.

경쟁 상태 해결책 - 동기화

경쟁 상태가 발생하는 것을 방지하기 위해 동기화 메커니즘을 사용하여 한번에 하나의 프로세스(또는 스레드)만 공유 자원에 접근 가능하도록 제어할 수 있다.

다음은 파이썬 threading 라이브러리의 락을 사용하여 한번에 하나의 스레드만 공유 자원에 접근하도록 만든 것이다. 락을 획득한 상태에서만 공유 자원에 접근할 수 있도록 하여 경쟁 상태를 방지할 수 있다.

import threading

# 공유 자원
counter = 0
lock = threading.Lock()  # Lock 객체 생성

def increment():
    global counter
    for _ in range(1000000):
        lock.acquire()
        counter += 1
        lock.release()

def decrement():
    global counter
    for _ in range(1000000):
        lock.acquire()
        counter -= 1
        lock.release()

# 두 개의 스레드 생성
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=decrement)

# 스레드 시작
thread1.start()
thread2.start()

# 두 스레드가 종료될 때까지 대기
thread1.join()
thread2.join()

print("Counter value:", counter)