경쟁 상태(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 같은 연산은 보통은 하나의 단계처럼 보이지만, 컴퓨터의 관점에서는 다음과 같은 세 가지 단계로 나뉜다.
- 메모리의 counter 값을 CPU 레지스터에 로드: CPU는 연산을 수행하기 전에 메모리에서 counter의 현재 값을 읽고 CPU 내부의 레지스터(Register)에 로드한다. CPU 레지스터에 로드한 후 연산을 수행함으로써 더 빠른 연산을 할 수 있다.
- 연산 수행: CPU는 레지스터에 로드한 counter의 값을 증가시키거나 감소시키는 연산을 수행한다. 예를 들어, counter += 1은 현재 counter의 값에 1을 더하는 연산을 수행하고, counter -= 1은 현재 counter의 값에서 1을 빼는 연산을 수행한다.
- 결과를 메모리의 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)
'운영체제' 카테고리의 다른 글
[OS] 가상 메모리 & 요구 페이징 (0) | 2024.06.12 |
---|---|
[OS] 새로운 프로세스는 어떻게 만들어질까? fork(), exec(), wait() (0) | 2024.05.02 |
쉘(Shell)과 커널(Kernel) (0) | 2023.09.18 |
[OS] 프로세스 스케줄링 (3) | 2023.07.20 |
크론탭(crontab)을 이용한 스케줄링 (0) | 2023.07.11 |