[운영체제] 3. 동기화와 교착 상태

index

여러 프로세스나 스레드가 공유 자원에 동시에 접근할 때 레이스 컨디션이 발생할 수 있다. 이 글에서는 동기화 기법과 교착 상태의 발생 조건 및 해결 방법을 다룬다.

1️⃣ 공유 자원과 임계 구역

프로세스 간 통신이나 스레드 간 통신이 이루어질 때, 여러 실행 주체가 하나의 자원을 공유하는 상황이 발생한다.

공유 자원 (Shared Resource)

공유 자원은 프로세스 또는 스레드가 공유하는 자원을 의미한다:

  • 메모리 영역
  • 파일
  • 전역 변수
  • 입출력장치

프로세스 간 공유 자원 예시

프로세스 A가 공유 메모리 공간에 데이터를 쓰고, 프로세스 B가 해당 메모리 공간을 읽는 경우, 두 프로세스는 공유 메모리라는 자원을 공유한다.

스레드 간 공유 자원 예시

동일한 프로세스의 스레드 A와 B가 각각 할당받은 파일을 수정하는 경우, 두 스레드는 파일 자원을 공유한다.

임계 구역 (Critical Section)

임계 구역은 공유 자원에 접근하는 코드 중 동시에 실행했을 때 문제가 발생할 수 있는 코드 영역이다.

1-1 임계 구역 문제 예시

다음과 같은 상황을 생각해보자:

실행 순서 결과
프로세스 A(쓰기) → 프로세스 B(읽기) 정상 동작
프로세스 B(읽기) → 프로세스 A(쓰기) 문제 발생 (아직 쓰이지 않은 메모리 읽기)

파일을 동시에 수정하는 스레드의 경우:

  1. 파일을 읽어 들임
  2. 원하는 내용을 작성
  3. 작성한 내용을 저장

초기 파일 값이 first이고, 스레드 A는 thread A를 추가, 스레드 B는 thread B를 추가하는 작업을 수행한다고 가정하자.

동시 실행 시 문제

스레드 A: 파일 읽기(first) → 파일 쓰기(first thread A) → 파일 저장
스레드 B: 파일 읽기(first) → 파일 쓰기(first thread B) → 파일 저장

결과: 스레드 A의 작업 내역이 반영되지 않음

이처럼 동시다발적으로 실행되는 프로세스 또는 스레드를 다룰 때는 임계 구역을 동시에 실행하지 않도록 유의해야 한다.

레이스 컨디션 (Race Condition)

레이스 컨디션은 프로세스 또는 스레드가 동시에 임계 구역의 코드를 실행하여 문제가 발생하는 상황을 말한다.

1-2 레이스 컨디션 발생 예시

다음 코드는 두 개의 스레드가 공유 변수를 동시에 수정하는 상황을 보여준다:

// filepath: 레이스 컨디션 예시
public class Race {
    static int sharedData = 0;  // 공유 데이터

    static class Increment implements Runnable {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                sharedData++;  // 공유 데이터 증가
            }
        }
    }

    static class Decrement implements Runnable {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                sharedData--;  // 공유 데이터 감소
            }
        }
    }
}

실행 결과

$ java Race
Final value of sharedData: -3394

$ java Race
Final value of sharedData: 1848

0이 출력될 것을 기대하지만, 실행할 때마다 일정하지 않은 결과가 도출된다. 이것이 바로 레이스 컨디션이다.

2️⃣ 동기화 (Synchronization)

레이스 컨디션을 방지하기 위해서는 프로세스와 스레드가 동기화되어야 한다.

동기화란 다음의 두 가지 조건을 준수하며 실행하는 것을 의미한다:

  • 실행 순서 제어: 프로세스 및 스레드를 올바른 순서로 실행하기
  • 상호 배제: 동시에 접근해서는 안 되는 자원에 하나의 프로세스 및 스레드만 접근하기

동기화의 두 가지 유형

유형 목적 예시
실행 순서 제어를 위한 동기화 프로세스를 올바른 순서로 실행 프로세스 A → 프로세스 B 순서 보장
상호 배제를 위한 동기화 하나의 프로세스만 자원에 접근 공유 변수에 한 번에 하나의 스레드만 접근

3️⃣ 뮤텍스 락 (Mutex Lock)

뮤텍스 락은 동시에 접근해서는 안 되는 자원에 동시 접근이 불가능하도록 상호 배제를 보장하는 동기화 도구다.

💡 Mutex의 의미

Mutex는 Mutual Exclusion(상호 배제)의 약자로, '상호 배제를 위한 락(lock)'을 의미한다.

뮤텍스 락의 원리

뮤텍스 락의 동작 원리는 다음과 같이 단순하다:

  • 임계 구역에 접근하려면 반드시 **락(lock)을 획득(acquire)**해야 함
  • 임계 구역에서의 작업이 끝나면 **락을 해제(release)**해야 함

3-1 뮤텍스 락의 구성 요소

뮤텍스 락은 다음과 같이 구성된다:

  • 변수(lock): 프로세스 및 스레드가 공유하는 변수
  • acquire(): 락을 획득하기 위한 함수 (한 번만 호출 가능)
  • release(): 획득한 락을 해제하기 위한 함수
lock.acquire()
// 임계 구역
lock.release()

3-2 뮤텍스 락의 동작 과정

공유 자원이 1개이고, P1과 P2가 공유 자원에 접근하려는 프로세스라고 가정하자:

  1. P1: acquire() 호출 → 임계 구역 진입
  2. P2: acquire() 호출 → lock을 획득하지 못해 임계 구역 접근 불가 (대기)
  3. P1: 임계 구역 작업 종료 → release() 호출
  4. P2: lock 획득 → 임계 구역 진입

3-3 뮤텍스 락 사용 예시

// filepath: 뮤텍스 락 적용 예시
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Mutex {
    static int sharedData = 0;
    static Lock lock = new ReentrantLock();  // 락 선언

    static class Increment implements Runnable {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                lock.lock();        // 락 획득
                try {
                    sharedData++;   // 임계 구역
                } finally {
                    lock.unlock();  // 락 해제
                }
            }
        }
    }
}

4️⃣ 세마포 (Semaphore)

세마포는 뮤텍스 락과 비슷하지만, 조금 더 일반화된 방식의 동기화 도구다. 공유 자원이 여러 개 있는 상황에서도 동기화가 가능하다.

💡 세마포의 유래

세마포는 철도 신호기에서 유래한 단어다. 신호기가 내려가 있을 때는 멈추라는 신호, 올라가 있을 때는 가도 좋다는 신호로 간주한다.

세마포의 구성 요소

세마포는 다음과 같이 구성된다:

  • 변수 S: 사용 가능한 공유 자원의 개수를 나타내는 변수
  • wait(): 임계 구역 진입 전 호출하는 함수
  • signal(): 임계 구역 진입 후 호출하는 함수
wait()
// 임계 구역
signal()

4-1 wait() 함수의 동작

wait() 함수는 다음과 같이 동작한다:

  1. 변수 S를 1 감소
  2. S가 0 이상이면 임계 구역 진입
  3. S가 0 미만이면 대기 상태로 전환
// filepath: wait() 함수 의사 코드
wait() {
    S--;                    // 1. S를 1 감소
    if (S < 0) {           // 2. S가 0보다 작은지 확인
        sleep();           // 3. 대기 상태로 전환
    }
}

4-2 signal() 함수의 동작

signal() 함수는 다음과 같이 동작한다:

  1. 변수 S를 1 증가
  2. S가 0 이하이면 대기 중인 프로세스를 준비 상태로 전환
// filepath: signal() 함수 의사 코드
signal() {
    S++;                    // 1. S를 1 증가
    if (S <= 0) {          // 2. S가 0 이하인지 확인
        wakeup(p);         // 3. 대기 프로세스를 준비 상태로 전환
    }
}

4-3 세마포 동작 예시

공유 자원이 2개, 접근하려는 프로세스가 3개(P1, P2, P3)인 경우:

  1. P1: wait() 호출 → S = 1 → 임계 구역 진입
  2. P2: wait() 호출 → S = 0 → 임계 구역 진입
  3. P3: wait() 호출 → S = -1 → 대기 상태로 전환
  4. P1: signal() 호출 → S = 0 → P3를 준비 상태로 전환
  5. P3: 임계 구역 진입
  6. P2: signal() 호출 → S = 1
  7. P3: signal() 호출 → S = 2

4-4 세마포 사용 예시

// filepath: 세마포 사용 예시
import java.util.concurrent.Semaphore;

public class Sem {
    static int sharedData = 0;
    static Semaphore semaphore = new Semaphore(1);  // 공유 자원 1개

    static class Increment implements Runnable {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                try {
                    semaphore.acquire();  // 세마포 획득
                    sharedData++;         // 임계 구역
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();  // 세마포 해제
                }
            }
        }
    }
}

💡 이진 세마포와 카운팅 세마포

세마포는 크게 두 가지 종류로 구분된다:

  • 이진 세마포(Binary Semaphore): S가 0과 1의 값만 가지는 세마포 (뮤텍스 락과 유사)
  • 카운팅 세마포(Counting Semaphore): 공유 자원이 여러 개 존재하는 경우에 사용

일반적으로 '세마포'라는 용어는 카운팅 세마포를 의미한다.

5️⃣ 조건 변수와 모니터

조건 변수 (Condition Variable)

조건 변수는 실행 순서 제어를 위한 동기화 도구로, 특정 조건 하에 프로세스를 실행하거나 일시 중단한다.

5-1 조건 변수의 함수

  • wait(): 호출한 프로세스 및 스레드의 상태를 대기 상태로 전환
  • signal(): wait()로 일시 중지된 프로세스 및 스레드의 실행을 재개

🎯 조건 변수의 원리

  • 특정 프로세스가 실행될 조건이 되지 않았을 때: wait()를 통해 실행 중단
  • 특정 프로세스가 실행될 조건이 충족되었을 때: signal()을 통해 실행 재개

5-2 조건 변수 동작 예시

프로세스 P1이 먼저 실행되다가 조건 변수에 의해 중단되고, 프로세스 P2가 2초간 실행을 마친 후 P1이 작업을 완료하는 상황:

P1: 먼저 시작 → 2초 대기(cv.wait()) → P2 완료 대기
P2: 2초 실행 → 실행 완료(cv.signal()) → P1 재개
P1: 다시 시작 → 종료

모니터 (Monitor)

모니터는 공유 자원과 그 공유 자원을 다루는 함수(인터페이스)로 구성된 동기화 도구다. 상호 배제와 실행 순서 제어를 위한 동기화를 모두 지원한다.

5-3 모니터의 동작 원리 (상호 배제)

  • 프로세스 및 스레드는 공유 자원에 접근하기 위해 정해진 인터페이스를 통해 모니터 내로 진입해야 함
  • 모니터 안에서 실행되는 프로세스 및 스레드는 항상 하나여야 함
  • 이미 모니터 내에 실행 중인 프로세스가 있다면 큐에서 대기

💡 큐(Queue)

는 먼저 삽입된 데이터를 먼저 활용할 수 있는 선입선출(FIFO) 구조의 자료구조다. 일종의 '줄'이라고 생각하면 된다.

5-4 모니터의 동작 원리 (실행 순서 제어)

모니터는 조건 변수를 함께 활용하여 실행 순서 제어를 구현한다.

프로세스 A가 먼저 실행되고, 다음으로 프로세스 B가 실행되어야 하는 경우:

  1. 프로세스 B가 먼저 모니터에 진입한 경우:

    • 프로세스 B는 프로세스 A의 실행 여부를 검사
    • 조건이 충족되지 않으면 cv.wait() 호출하여 대기 상태로 전환
    • 프로세스 A가 모니터 내에서 실행 완료 후 cv.signal() 호출
    • 프로세스 B가 모니터 안으로 재진입하여 실행
  2. 프로세스 A가 먼저 모니터에 진입한 경우:

    • 프로세스 A가 실행 완료
    • 프로세스 B가 모니터 내로 진입하여 실행

5-5 모니터 사용 예시

자바의 synchronized 키워드는 모니터를 사용하는 대표적 예시다:

// filepath: synchronized 키워드 예시
public synchronized void example(int value) {
    this.count += value;  // 하나의 스레드만 실행 가능
}

6️⃣ 스레드 안전 (Thread Safety)

스레드 안전이란 멀티스레드 환경에서 어떤 변수나 함수, 객체에 동시 접근이 이루어져도 실행에 문제가 없는 상태를 의미한다.

스레드 안전의 조건

  • 레이스 컨디션이 발생하지 않음
  • 여러 스레드에 의해 동시에 호출되어도 올바르게 동작

6-1 스레드 안전 예시 (Java)

Vector 클래스 - 스레드 안전

// filepath: Vector 클래스의 add 메서드
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

Vectoradd() 메서드는 synchronized 키워드로 구현되어 있어 스레드 안전성이 보장된다.

ArrayList 클래스 - 스레드 안전하지 않음

// filepath: ArrayList 클래스의 add 메서드
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

ArrayListadd() 메서드는 synchronized 키워드가 없어 스레드 안전성이 보장되지 않는다.

6-2 스레드 안전 검증

두 개의 스레드로 각각 VectorArrayListadd()를 호출한 결과:

ArrayList size: 5012    # 동기화되지 않아 레이스 컨디션 발생
Vector size: 10000      # 동기화되어 일정한 결과 유지

7️⃣ 교착 상태 (Deadlock)

교착 상태란 일어나지 않을 사건을 기다리며 프로세스의 진행이 멈춰 버리는 현상을 말한다.

교착 상태의 예시

  • 프로세스 A: 자원 X를 점유한 채 자원 Y의 사용이 끝나기를 기다림
  • 프로세스 B: 자원 Y를 점유한 채 자원 X의 사용이 끝나기를 기다림

결과: 두 프로세스 모두 서로가 가진 자원을 기다리며 실행하지 못함

8️⃣ 교착 상태의 발생 조건

교착 상태가 발생하려면 다음 네 가지 조건이 모두 만족되어야 한다. 하나라도 만족하지 않으면 교착 상태는 발생하지 않는다.

교착 상태의 4가지 필요 조건

8-1 상호 배제 (Mutual Exclusion)

한 번에 하나의 프로세스만 해당 자원을 이용할 수 있는 상황이다.

  • 한 프로세스가 사용하는 자원을 다른 프로세스가 사용할 수 없음

8-2 점유와 대기 (Hold and Wait)

한 프로세스가 자원을 할당받은 상태(점유)에서 다른 자원을 기다리는(대기) 상황이다.

8-3 비선점 (No Preemption)

어떤 프로세스도 다른 프로세스의 자원을 강제로 빼앗지 못하는 상황이다.

  • 자원을 이용하는 프로세스의 작업이 끝나야만 자원을 이용할 수 있음

8-4 원형 대기 (Circular Wait)

프로세스와 프로세스가 요청한 자원이 원의 형태를 이루는 상황이다.

  • 각각의 프로세스가 서로 점유한 자원을 할당받기 위해 원형으로 대기

9️⃣ 교착 상태의 해결 방법

운영체제는 다음 세 가지 방식으로 교착 상태를 해결할 수 있다.

교착 상태 예방 (Deadlock Prevention)

교착 상태 예방은 교착 상태를 발생시키는 4가지 필요 조건 중 하나를 충족하지 못하게 하는 방법이다.

9-1 예방 기법 예시

점유와 대기 조건 제거

  • 한 프로세스에 필요한 자원들을 몰아서 할당
  • 다음 프로세스에 필요한 자원을 몰아서 할당
  • 결과: 점유와 대기 조건 불만족 → 교착 상태 발생하지 않음

원형 대기 조건 제거

  • 모든 자원에 번호를 매김 (자원 X: 0, 자원 Y: 1, 자원 Z: 2)
  • 오름차순으로만 자원을 할당받도록 제한
  • 결과: 원형 대기 조건 불만족 → 교착 상태 발생하지 않음

교착 상태 회피 (Deadlock Avoidance)

교착 상태 회피는 교착 상태가 발생하지 않을 정도로만 조심하면서 자원을 할당하는 방법이다.

9-2 회피의 기본 개념

교착 상태를 한정된 자원의 무분별한 할당으로 인해 발생하는 문제로 간주한다:

  • 자원이 충분하고 프로세스가 적은 자원만 요구 → 교착 상태 발생하지 않음
  • 자원이 한정되고 프로세스가 많은 자원을 요구 → 교착 상태 발생 위험 증가

💡 은행원 알고리즘 (Banker's Algorithm)

교착 상태 회피를 위한 대표적인 알고리즘으로, 프로세스가 요청한 자원을 할당했을 때 교착 상태가 발생할 가능성을 미리 계산하여 자원을 할당한다.

교착 상태 검출 후 회복 (Deadlock Detection and Recovery)

교착 상태 검출 후 회복은 교착 상태의 발생을 인정하고 처리하는 사후 조치다.

9-3 검출 후 회복의 동작 방식

검출

  • 프로세스가 자원을 요구할 때마다 자원을 할당
  • 주기적으로 교착 상태의 발생 여부를 검사

교착 상태가 검출되면 다음 방법으로 회복한다:

방법 설명
자원 선점을 통한 회복 교착 상태가 해결될 때까지 다른 프로세스로부터 강제로 자원을 빼앗아 한 프로세스에 몰아서 할당
프로세스 강제 종료 교착 상태에 놓인 프로세스를 강제로 종료

🔟 정리

동기화와 교착 상태의 핵심 개념을 정리하면 다음과 같다:

동기화 기법

  • 뮤텍스 락: 하나의 공유 자원에 대한 상호 배제 보장
  • 세마포: 여러 개의 공유 자원에 대한 상호 배제 보장
  • 조건 변수: 실행 순서 제어
  • 모니터: 상호 배제와 실행 순서 제어를 모두 지원

교착 상태

  • 발생 조건: 상호 배제, 점유와 대기, 비선점, 원형 대기 (4가지 모두 만족)
  • 해결 방법: 예방, 회피, 검출 후 회복

이러한 동기화 기법과 교착 상태 해결 방법은 안정적이고 효율적인 멀티프로세스/멀티스레드 환경을 구축하는 핵심 메커니즘이다.

© 2026, Design & Developed by 정인영