새소식

xub2의 개발 새발 🐾
🔬 CS/OS

프로세스 동기화 & 데드락

2026. 7. 21. 10:30

  • -

이번엔 동시성 문제 (Race Condition)이 생기는 이유부터 데드락, 그리고 데드락 해결까지 다룰 예정이다.


 

1. Race Condition (동시성 문제)

Race Condition이란, 두 개의 연산이 동시에 공유 자원에 접근할 때 결과가 일관성을 보장받지 못하는 상태를 의미한다.

Race의 뜻 그대로, 여러 프로세스/스레드가 공유 자원에 먼저 도달해서 작업을 끝내려고 "경쟁"하는 상황을 비유한 이름이다.

동시성 문제는 다음 두 조건이 동시에 충족될 때 발생한다.

  • 공유 자원 사용: 여러 스레드/프로세스가 동일한 자원(변수, 메모리 등)을 동시에 사용함 (예: 전역 변수, 파일, DB)
  • 동시성: 두 스레드/프로세스가 동시에 실행되며, 실행 순서를 운영체제가 제어하지 못함 (예: 문맥 전환으로 스레드 작업이 중단됐다가 다시 실행됨)

 

Race Condition 예시

공유된 은행 계좌(초기 잔액 100원)에 스레드 A는 50원 입금, 스레드 B는 30원 출금을 동시에 수행하는 상황.

읽기 → 계산 → 저장 3단계로 나눠서 보면:

  1. 읽기: A는 잔액 100을 읽음 / B도 잔액 100을 읽음
  2. 계산: A는 100+50=150 계산 / B는 100-30=70 계산
  3. 저장: A가 150을 저장 → B가 70을 저장 (덮어씀)

최종 잔액은 70원. 원래대로면 100+50-30=120원이어야 하는데, A의 결과가 B의 결과에 덮어씌워지면서 틀린 값이 나온다. "동시에 값을 읽고 각자 계산해서 나중에 쓰는" 구조 자체가 문제의 원인이다.

 

OS 내부에서 Race Condition이 생기는 경우 및 해결 방법

문제 상황 해결
커널 모드에서 데이터를 로드해 수행하던 중 인터럽트가 발생해서 같은 데이터에 접근하는 경우 중요한 데이터에 접근 중일 때는 인터럽트를 바로 처리하지 않고 해당 작업 이후로 지연 처리
프로세스가 커널 모드 수행 중인데 문맥 교환이 발생하는 경우 커널 모드 수행 중엔 CPU를 빼앗기지 않고(비선점), 사용자 모드로 돌아갈 때 CPU 반납
다중 CPU 환경에서 여러 CPU가 같은 데이터에 접근하는 경우 공유 데이터에 락(lock/unlock)을 걸어서 다른 접근을 차단 (모니터 락, concurrent lock 등)

2. 임계 영역 (Critical Section)

임계 영역이란 두 개 이상의 프로세스나 스레드가 동시에 접근하면 안 되는, 공유 자원을 사용하는 코드 영역을 의미한다.

데이터 일관성을 지키려면 한 번에 하나의 프로세스/스레드만 이 영역을 실행하도록 제어해야 한다.

 

임계 영역 문제(Critical Section Problem)

여러 프로세스가 동시에 임계 영역에 접근하면 Race Condition이 발생할 수 있다.

그래서 운영체제는 다음 세 가지 조건을 만족시켜서 이 문제를 해결한다.

  • 상호 배제 (Mutual Exclusion): 한 번에 하나의 프로세스만 크리티컬 섹션에 진입할 수 있어야 함
  • 진행 조건 (Progress): 크리티컬 섹션에 아무도 없으면 누군가는 들어갈 수 있어야 함
  • 유한 대기 (Bounded Waiting): 대기 중인 프로세스가 일정 시간 내에는 반드시 크리티컬 섹션에 진입할 수 있어야 함

 

각 조건이 깨졌을 때 어떤 문제가 생기는지 보면 이해가 좀 더 쉽다.

  • 상호 배제 불충족: 위 은행 계좌 예시처럼 여러 프로세스가 동시에 접근 → 데이터 일관성 붕괴
  • 진행 조건 불충족: 아무도 크리티컬 섹션에 없는데도 아무도 못 들어감 → 자원 낭비
  • 유한 대기 불충족: 우선순위 기반으로만 접근 순서를 정하면, 높은 우선순위 프로세스가 계속 들어와서 낮은 우선순위 프로세스는 계속 기다리기만 함 (CPU 스케줄링에서 본 starvation과 같은 종류의 문제)

3. 해결 방법 : 세마포어(Semaphore)

세마포어는 운영체제에서 여러 프로세스/스레드가 공유 자원에 접근하는 걸 동기화하기 위해 쓰는 기술이다.

이는 정수 값으로 자원 접근 가능 여부를 제어한다.

  • 값이 0보다 크면 자원을 사용할 수 있고, 0이면 자원이 모두 사용 중
  • P(S) 연산: 자원 획득 연산. 세마포어 값을 1 감소시킴 (값이 0이면 대기)
  • V(S) 연산: 자원 반납 연산. 세마포어 값을 1 증가시킴

 

예시로 보는 동작

공유 자원을 쓰는 프로세스 A, B, C가 있고 자원이 2개만 있다고 가정.

  1. 초기 세마포어 값 = 2
  2. A가 P(S) 호출 → 값 2→1, 자원 사용
  3. B가 P(S) 호출 → 값 1→0, 자원 사용
  4. C가 P(S) 호출 → 값이 0이라 대기
  5. A가 작업 완료 후 V(S) 호출 → 값 0→1, C가 자원 사용 가능

 

세마포어의 종류

  • 카운팅 세마포어(Counting Semaphore): 0 이상의 정수 값을 가지며 동시 접근 가능한 자원의 개수를 나타냄 (예: 프린터 3대 → 최대 3개 프로세스가 동시 사용 가능)
  • 이진 세마포어(Binary Semaphore) = 뮤텍스(Mutex): 값이 0 또는 1만 가능. 한 번에 하나의 프로세스만 접근 가능

 

예시 수도 코드

semaphore S = 1; // 세마포어 초기화

void process() {
    while (true) {
        P(S); // 세마포어 감소 (Critical Section 진입 요청)

        // Critical Section: 공유 자원 처리 로직

        V(S); // 세마포어 증가 (Critical Section에서 나옴)

        // Remainder Section: 공유 자원과 무관한 나머지 작업
    }
}

 

 

대기 상태 처리 방식 두 가지

Busy-Wait

  • 자원을 쓸 수 있을 때까지 세마포어 값을 계속 확인하며 대기 (while (S <= 0) {})
  • CPU를 계속 쓰면서 루프를 반복하니까 자원 낭비가 있음

Block/Wakeup

  • 자원이 사용 중이면 프로세스를 Blocked 상태로 전환하고, 자원이 가용해지면 깨워서(Wakeup) 실행 재개
  • P(S) 호출 시 값이 0 미만이면 프로세스가 block() 호출 → 운영체제가 이 프로세스를 세마포어의 Wait Queue에 넣고 CPU 제어권을 빼앗음
  • V(S) 호출 시 Wait Queue에서 대기 중인 다른 프로세스를 깨워서 실행 재개
  • CPU를 낭비하지 않고 효율적으로 대기함

다만 무조건 Block/Wakeup이 좋은 건 아니다. Critical Section 실행 시간이 짧으면 Block/Wakeup의 오버헤드(큐에 넣고 빼고 깨우는 과정)가 오히려 더 커질 수 있어서, 이런 경우엔 Busy-Wait이 더 효율적일 수도 있다.


4. 데드락(Deadlock, 교착 상태)

두 개 이상의 프로세스가 서로가 소유한 자원을 요청하며 대기 상태에 빠져, 더 이상 실행을 진행할 수 없는 상황이 데드락이다.

이는 대부분 공유 자원이나 동기화 기법을 잘못 설계했을 때 발생한다.

 

데드락 발생 조건 (4가지가 동시에 충족될 때 발생)

  • 상호 배제(Mutual Exclusion): 자원은 한 번에 하나의 프로세스만 사용 가능 (예: 프린터, 파일 접근)
  • 점유 대기(Hold and Wait): 프로세스가 이미 할당된 자원을 점유한 채로, 추가로 다른 자원을 요청하며 대기함
  • 비선점(No Preemption): 이미 할당된 자원은 해당 프로세스가 끝낼 때까지 강제로 뺏을 수 없음
  • 순환 대기(Circular Wait): 프로세스들이 순환 형태로 자원을 요청하며 대기 (예: A가 B의 자원을 요청, B가 A의 자원을 요청)

대표적인 예시: P1이 자원 A를 가진 채 자원 B를 요구하고, P2가 자원 B를 가진 채 자원 A를 요구하면 P1, P2 둘 다 더 이상 진행이 불가능해진다.

참고로 자원 할당 그래프에 사이클이 있다고 무조건 데드락은 아니다. 예를 들어 위 예시에서 공유 자원 A가 2개 있다면, 사이클이 있어도 P2가 작업을 진행할 수 있어서 데드락이 발생하지 않는다. 즉 사이클은 데드락의 필요조건이지 충분조건은 아니다.

 

데드락 해결 방법 3가지 (순서대로 강도가 높아짐)

예방 (Prevention)

예방(Prevention)은 데드락이 발생하려면 위 4가지 조건이 모두 필요하니까, 그중 하나 이상을 애초에 충족 못 하게 설계해서 원천적으로 방지한다.

방지 대상 방법 다단점
상호 배제 방지 공유 자원을 여러 프로세스가 동시에 쓸 수 있게 설계 (예: 읽기 전용 데이터는 동시 접근 허용) 쓰기 작업은 여전히 상호 배제 필요
점유 대기 방지 필요한 자원을 한꺼번에 요청하도록 강제 자원 낭비 가능 (필요 없는 자원도 미리 점유)
비선점 방지 자원이 점유 중이면 강제로 빼앗아 다른 프로세스가 쓸 수 있게 함 데이터 일관성 문제, 오버헤드 발생 가능
순환 대기 방지 자원에 번호를 매기고 항상 번호가 증가하는 순서로만 요청하게 함 요청 순서를 엄격히 지켜야 해서 유연성이 떨어짐

 

회피 (Avoidance)

 회피(Avoidance)는 시스템의 현재 상태를 평가해서, 데드락이 발생하지 않을 경우에만 자원을 할당한다. 프로세스의 최대 자원 요구량을 미리 알아야 한다.

  • 동작: 프로세스가 자원을 요청하면, 할당했다고 가정했을 때의 시스템 상태를 시뮬레이션 → 안전 상태(모든 프로세스가 실행을 완료할 수 있는 상태)면 할당, 아니면 요청 거절
  • 장점: 데드락 발생 가능성을 최소화
  • 단점: 자원의 최대 요구량을 사전에 알아야 하고, 계산 비용이 증가함

 

탐지 및 회복 (Detection & Recovery)

  • 탐지: 데드락이 발생했는지 주기적으로 검사. 자원 할당 그래프에서 사이클 존재 여부를 확인
  • 회복: 데드락을 탐지한 후 문제를 해결하는 조치
    • 프로세스 종료: 데드락을 유발하는 프로세스를 하나 이상 종료 (전체 종료 또는 우선순위 낮은 것부터 하나씩)
    • 자원 선점: 특정 프로세스에서 자원을 강제로 회수해서 다른 프로세스가 먼저 끝내도록 하고, 선점당한 프로세스는 나중에 다시 실행. 단점은 데이터 일관성 문제가 생길 수 있음

 

여담 : 실무에서 택하는 방식

예방·회피·탐지 및 회복 모두 나름의 비용(복잡한 알고리즘, 유연성 저하, 계산 비용)이 있고, 실제로 데드락 발생 확률 자체가 낮은 경우가 많아서 아예 무시하는 전략을 쓰기도 한다. 이 경우 데드락이 실제로 생기면 사용자가 수동으로 문제를 해결한다 (프로세스 종료 및 재시작). 


정리

여러 프로세스가 공유 자원을 동시에 건드리면 Race Condition이 생기고, 이를 막기 위한 코드 영역이 크리티컬 섹션이다. 세마포어(P/V 연산)로 한 번에 하나씩만 접근하게 만들 수 있지만, 자원을 점유한 채 서로를 기다리게 설계하면 데드락에 빠질 수 있다. 데드락은 예방·회피·탐지 및 회복 순으로 강도 높은 해결책이 있지만, 실무에서는 발생 확률과 비용을 따져 아예 무시하는 전략도 쓴다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.