Skip to content

Split View: [운영체제] 06. 프로세스 동기화 도구

|

[운영체제] 06. 프로세스 동기화 도구

배경: 경쟁 조건

여러 프로세스(또는 스레드)가 공유 데이터에 동시에 접근하면, 실행 순서에 따라 결과가 달라질 수 있다. 이를 **경쟁 조건(Race Condition)**이라 한다.

// 경쟁 조건 예시: 두 스레드가 공유 변수 count를 증가
// count의 초기값이 5일 때

// 스레드 A: count++
// 내부적으로:
//   register_A = count        (register_A = 5)
//   register_A = register_A + 1  (register_A = 6)
//   count = register_A        (count = 6)

// 스레드 B: count++
// 내부적으로:
//   register_B = count        (register_B = 5)
//   register_B = register_B + 1  (register_B = 6)
//   count = register_B        (count = 6)

// 교차 실행 시나리오:
// T0: A가 register_A = count         (register_A = 5)
// T1: A가 register_A = register_A + 1 (register_A = 6)
// T2: B가 register_B = count         (register_B = 5)  <-- 아직 5!
// T3: B가 register_B = register_B + 1 (register_B = 6)
// T4: A가 count = register_A         (count = 6)
// T5: B가 count = register_B         (count = 6)
// 결과: count = 6 (기대값 7이 아님!)

임계 영역 문제 (Critical Section Problem)

임계 영역(Critical Section)은 공유 데이터에 접근하는 코드 영역이다.

[프로세스 구조]

do {
    [진입 영역(Entry Section)]
        -- 임계 영역에 들어가기 위한 허가 요청 --

    [임계 영역(Critical Section)]
        -- 공유 데이터 접근 --

    [퇴출 영역(Exit Section)]
        -- 임계 영역 사용 완료 알림 --

    [나머지 영역(Remainder Section)]
} while (true);

임계 영역 문제의 해결 조건 세 가지는 다음과 같다.

  1. 상호 배제(Mutual Exclusion): 한 프로세스가 임계 영역을 실행 중이면, 다른 프로세스는 들어갈 수 없음
  2. 진행(Progress): 임계 영역에 아무도 없을 때, 들어가려는 프로세스 중 하나가 선택되어야 함. 결정이 무한히 지연되면 안 됨
  3. 한정 대기(Bounded Waiting): 프로세스가 임계 영역 진입을 요청한 후, 다른 프로세스가 임계 영역에 들어가는 횟수에 한계가 있어야 함

피터슨 해법 (Peterson's Solution)

두 프로세스 간의 임계 영역 문제를 소프트웨어로 해결하는 고전적 알고리즘이다.

// 피터슨 해법 (두 프로세스 i, j)
int turn;           // 누구 차례인지
bool flag[2];       // 임계 영역 진입 의사

// 프로세스 i의 코드
void process_i() {
    while (true) {
        flag[i] = true;     // 나(i)는 들어가고 싶다
        turn = j;           // 상대방(j)에게 양보
        while (flag[j] && turn == j)
            ;               // 상대가 원하고, 상대 차례면 대기

        // 임계 영역
        // ... 공유 데이터 접근 ...

        flag[i] = false;    // 나는 다 썼다

        // 나머지 영역
    }
}

피터슨 해법은 세 가지 조건(상호 배제, 진행, 한정 대기)을 모두 만족한다. 하지만 현대 컴퓨터 아키텍처에서는 컴파일러나 프로세서의 명령어 재배치(reordering) 때문에 올바르게 동작하지 않을 수 있다.


하드웨어 동기화 지원

메모리 배리어 (Memory Barrier)

메모리 배리어(또는 메모리 펜스)는 프로세서에게 배리어 전후의 메모리 접근 순서를 보장하도록 지시한다.

// 메모리 배리어 사용 예시
// 스레드 1
while (!flag)
    memory_barrier();  // flag 읽기가 이 시점 이전으로 재배치되지 않음
print(x);

// 스레드 2
x = 100;
memory_barrier();      // x 쓰기가 flag 쓰기 이전에 완료됨을 보장
flag = true;

CAS (Compare-And-Swap)

CAS는 원자적(atomic) 하드웨어 명령어로, 동기화의 기본 빌딩 블록이다.

// CAS의 논리 (실제로는 하나의 원자적 명령어)
bool compare_and_swap(int *value, int expected, int new_value) {
    if (*value == expected) {
        *value = new_value;
        return true;
    }
    return false;
}

// CAS를 이용한 원자적 증가
void atomic_increment(int *counter) {
    int old_val;
    do {
        old_val = *counter;
    } while (!compare_and_swap(counter, old_val, old_val + 1));
    // 성공할 때까지 재시도
}

// CAS를 이용한 상호 배제
int lock = 0;

void acquire() {
    while (!compare_and_swap(&lock, 0, 1))
        ;  // lock이 0(사용 가능)이면 1(사용 중)로 변경
}

void release() {
    lock = 0;
}

원자적 변수 (Atomic Variables)

CAS 같은 하드웨어 명령어를 기반으로, 기본 데이터 타입에 대한 원자적 연산을 제공한다.

// C11 원자적 변수
#include <stdatomic.h>

atomic_int counter = 0;

void increment() {
    atomic_fetch_add(&counter, 1);  // 원자적 증가
}

// Java의 원자적 변수
// import java.util.concurrent.atomic.AtomicInteger;
// AtomicInteger counter = new AtomicInteger(0);
// counter.incrementAndGet();  // 원자적 증가

원자적 변수는 단일 변수에 대한 경쟁 조건은 해결하지만, 복잡한 임계 영역 문제의 일반적 해결책은 되지 못한다.


뮤텍스 락 (Mutex Lock)

뮤텍스는 가장 단순한 동기화 도구로, 임계 영역을 보호하는 잠금 장치다.

// 뮤텍스 사용 패턴
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void critical_section() {
    pthread_mutex_lock(&mutex);     // 잠금 획득 (acquire)

    // 임계 영역
    // 공유 데이터 접근

    pthread_mutex_unlock(&mutex);   // 잠금 해제 (release)
}
[뮤텍스 동작]

스레드 A                   스레드 B
   |                          |
   |-- lock() 성공 ----       |
   |   (임계 영역 진입)        |-- lock() 시도
   |   공유 데이터 접근        |   (대기/블로킹)
   |                          |   ...
   |-- unlock() -------       |
   |                    ----->|-- lock() 성공
   |                          |   (임계 영역 진입)
   |                          |   공유 데이터 접근
   |                          |-- unlock()

스핀락(Spinlock): lock을 얻을 때까지 반복 확인하며 대기(바쁜 대기, busy waiting). 멀티코어에서 짧은 임계 영역에 적합하다. 컨텍스트 스위치 오버헤드를 피할 수 있기 때문이다.

// 스핀락 구현 (CAS 기반)
typedef struct {
    atomic_flag flag;
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (atomic_flag_test_and_set(&lock->flag))
        ;  // 잠금될 때까지 반복 (바쁜 대기)
}

void spin_unlock(spinlock_t *lock) {
    atomic_flag_clear(&lock->flag);
}

세마포어 (Semaphore)

세마포어는 뮤텍스보다 일반적인 동기화 도구다. 정수 변수로 wait()와 signal() 연산만 허용한다.

// 세마포어의 기본 연산 (원자적)
wait(S) {          // P 연산 또는 acquire
    while (S <= 0)
        ;          // 대기
    S--;
}

signal(S) {        // V 연산 또는 release
    S++;
}

세마포어의 종류

  • 카운팅 세마포어: 값의 범위에 제한 없음. 가용 자원의 개수를 추적
  • 이진 세마포어: 0 또는 1만 가능. 뮤텍스와 유사
// 카운팅 세마포어 사용 예: 최대 5개 동시 접속
#include <semaphore.h>

sem_t connection_pool;

void init() {
    sem_init(&connection_pool, 0, 5);  // 초기값 5
}

void handle_request() {
    sem_wait(&connection_pool);    // 연결 획득 (남은 수 감소)

    // 데이터베이스 작업 수행
    // ...

    sem_post(&connection_pool);    // 연결 반환 (남은 수 증가)
}

세마포어를 이용한 순서 제어

// S1이 S2보다 먼저 실행되도록 보장
sem_t sync_sem;
sem_init(&sync_sem, 0, 0);  // 초기값 0

// 프로세스 1
void process1() {
    S1;                     // 먼저 실행할 문장
    sem_post(&sync_sem);    // signal
}

// 프로세스 2
void process2() {
    sem_wait(&sync_sem);    // wait (S1 완료까지 대기)
    S2;                     // S1 이후에 실행
}

바쁜 대기 없는 세마포어

실제 구현에서는 바쁜 대기 대신 대기 큐를 사용한다.

typedef struct {
    int value;
    struct list_head waiting_list;  // 대기 프로세스 목록
} semaphore;

void wait(semaphore *S) {
    S->value--;
    if (S->value < 0) {
        // 현재 프로세스를 대기 목록에 추가
        add_to_list(&S->waiting_list, current_process);
        block();  // 프로세스를 대기 상태로 전환
    }
}

void signal(semaphore *S) {
    S->value++;
    if (S->value <= 0) {
        // 대기 목록에서 프로세스 하나를 깨움
        process *P = remove_from_list(&S->waiting_list);
        wakeup(P);  // P를 준비 상태로 전환
    }
}

모니터 (Monitor)

세마포어는 강력하지만 사용하기 어렵고 오류가 발생하기 쉽다. 모니터는 프로그래밍 언어 수준에서 동기화를 지원하는 고수준 구조체다.

[모니터 구조]

+------------------------------------+
|           monitor                  |
|                                    |
|  공유 데이터                        |
|  +-----------+                     |
|  | 변수들     |                     |
|  +-----------+                     |
|                                    |
|  조건 변수                          |
|  condition x, y;                   |
|                                    |
|  procedure P1() { ... }           |
|  procedure P2() { ... }           |
|  procedure P3() { ... }           |
|                                    |
|  초기화 코드                        |
|                                    |
+------------------------------------+
      ^          ^          ^
      |          |          |
   스레드1    스레드2    스레드3
   (한 번에 하나만 모니터 내부 실행)

조건 변수 (Condition Variable)

모니터 내에서 특정 조건이 만족될 때까지 대기하기 위해 사용한다.

  • x.wait(): 호출한 프로세스를 조건 x의 대기 큐에 넣고 모니터를 해제
  • x.signal(): 조건 x의 대기 큐에서 하나의 프로세스를 깨움
// Java에서 모니터 패턴 (synchronized 키워드)
public class BoundedBuffer {
    private final Object[] buffer;
    private int count = 0;
    private int in = 0;
    private int out = 0;

    public BoundedBuffer(int size) {
        buffer = new Object[size];
    }

    // synchronized: 한 번에 하나의 스레드만 실행
    public synchronized void produce(Object item)
            throws InterruptedException {
        while (count == buffer.length) {
            wait();  // 버퍼가 가득 차면 대기
        }

        buffer[in] = item;
        in = (in + 1) % buffer.length;
        count++;

        notifyAll();  // 대기 중인 소비자 깨움
    }

    public synchronized Object consume()
            throws InterruptedException {
        while (count == 0) {
            wait();  // 버퍼가 비어있으면 대기
        }

        Object item = buffer[out];
        out = (out + 1) % buffer.length;
        count--;

        notifyAll();  // 대기 중인 생산자 깨움
        return item;
    }
}
// Pthreads 조건 변수 사용
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

// 대기하는 스레드
void *waiter(void *arg) {
    pthread_mutex_lock(&mutex);

    while (!ready) {
        // 조건이 만족될 때까지 대기
        // mutex를 해제하고 대기, 깨어나면 다시 획득
        pthread_cond_wait(&cond, &mutex);
    }

    printf("조건 충족! 진행합니다.\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 신호를 보내는 스레드
void *signaler(void *arg) {
    pthread_mutex_lock(&mutex);

    ready = 1;
    pthread_cond_signal(&cond);  // 대기 중인 스레드 깨움

    pthread_mutex_unlock(&mutex);
    return NULL;
}

활성 문제 (Liveness Problems)

동기화 도구를 부적절하게 사용하면 프로세스가 영원히 진행하지 못하는 상황이 발생할 수 있다.

교착 상태 (Deadlock)

두 개 이상의 프로세스가 서로 상대방이 보유한 자원을 기다리며 영원히 대기하는 상태다.

[교착 상태 예시]

스레드 A                    스레드 B
lock(mutex1);              lock(mutex2);
  // mutex1 보유              // mutex2 보유
lock(mutex2);              lock(mutex1);
  // mutex2 대기!             // mutex1 대기!
  // 서로 상대방의 자원을 기다리며 교착

해결: 자원 획득 순서를 항상 동일하게 유지
스레드 A                    스레드 B
lock(mutex1);              lock(mutex1);  // 같은 순서로 획득
lock(mutex2);              lock(mutex2);

기아 (Starvation)

프로세스가 필요한 자원을 영원히 얻지 못하는 상태다. 예를 들어, 우선순위가 낮은 프로세스가 높은 우선순위 프로세스에 의해 계속 밀리는 경우다.

우선순위 역전 (Priority Inversion)

높은 우선순위 프로세스가 낮은 우선순위 프로세스가 보유한 자원을 기다리게 되는 상황이다.

[우선순위 역전 시나리오]

우선순위: H > M > L

1. L이 자원 R을 획득하고 실행 중
2. H가 도착, R을 요청 -> L이 보유 중이므로 대기
3. M이 도착, L을 선점 -> M이 실행
4. HLR을 반납해야 하는데, LM에 의해 선점됨
5. 결과: MH보다 먼저 실행! (우선순위 역전)

해결: 우선순위 상속 프로토콜
LR을 보유한 동안 H의 우선순위를 상속받음
-> LM에 의해 선점되지 않음 -> LR을 빨리 반납

화성 탐사선 패스파인더에서 우선순위 역전 버그가 실제로 발생한 사례가 유명하다. 우선순위 상속 프로토콜을 활성화하여 해결했다.


정리

프로세스 동기화는 공유 자원에 대한 경쟁 조건을 해결하기 위해 필수적이다. 하드웨어 수준의 CAS와 원자적 변수부터, 뮤텍스와 세마포어, 그리고 고수준의 모니터까지 다양한 도구가 존재한다. 이러한 도구를 올바르게 사용하지 않으면 교착 상태, 기아, 우선순위 역전 같은 활성 문제가 발생할 수 있다.

[OS Concepts] 06. Process Synchronization Tools

Background: Race Condition

When multiple processes (or threads) access shared data concurrently, the result may vary depending on the execution order. This is called a race condition.

// Race condition example: Two threads incrementing shared variable count
// Initial value of count is 5

// Thread A: count++
// Internally:
//   register_A = count        (register_A = 5)
//   register_A = register_A + 1  (register_A = 6)
//   count = register_A        (count = 6)

// Thread B: count++
// Internally:
//   register_B = count        (register_B = 5)
//   register_B = register_B + 1  (register_B = 6)
//   count = register_B        (count = 6)

// Interleaved execution scenario:
// T0: A sets register_A = count         (register_A = 5)
// T1: A sets register_A = register_A + 1 (register_A = 6)
// T2: B sets register_B = count         (register_B = 5)  <-- still 5!
// T3: B sets register_B = register_B + 1 (register_B = 6)
// T4: A sets count = register_A         (count = 6)
// T5: B sets count = register_B         (count = 6)
// Result: count = 6 (not the expected 7!)

Critical Section Problem

The critical section is the code region that accesses shared data.

[Process Structure]

do {
    [Entry Section]
        -- Request permission to enter critical section --

    [Critical Section]
        -- Access shared data --

    [Exit Section]
        -- Notify completion of critical section usage --

    [Remainder Section]
} while (true);

The three conditions for solving the critical section problem are as follows.

  1. Mutual Exclusion: If one process is executing in its critical section, no other process can enter
  2. Progress: If no process is in the critical section, a process that wants to enter must be selected. The decision cannot be postponed indefinitely
  3. Bounded Waiting: After a process requests entry to the critical section, there must be a limit on the number of times other processes enter their critical sections

Peterson's Solution

A classic software-based algorithm that solves the critical section problem for two processes.

// Peterson's Solution (two processes i, j)
int turn;           // Whose turn it is
bool flag[2];       // Intent to enter critical section

// Code for process i
void process_i() {
    while (true) {
        flag[i] = true;     // I (i) want to enter
        turn = j;           // Yield to the other (j)
        while (flag[j] && turn == j)
            ;               // Wait if the other wants it and it's their turn

        // Critical section
        // ... access shared data ...

        flag[i] = false;    // I'm done

        // Remainder section
    }
}

Peterson's solution satisfies all three conditions (mutual exclusion, progress, bounded waiting). However, on modern computer architectures, it may not work correctly due to instruction reordering by the compiler or processor.


Hardware Synchronization Support

Memory Barrier

A memory barrier (or memory fence) instructs the processor to guarantee the order of memory accesses before and after the barrier.

// Memory barrier usage example
// Thread 1
while (!flag)
    memory_barrier();  // Ensures flag read is not reordered before this point
print(x);

// Thread 2
x = 100;
memory_barrier();      // Ensures x write completes before flag write
flag = true;

CAS (Compare-And-Swap)

CAS is an atomic hardware instruction and is the fundamental building block of synchronization.

// CAS logic (actually a single atomic instruction)
bool compare_and_swap(int *value, int expected, int new_value) {
    if (*value == expected) {
        *value = new_value;
        return true;
    }
    return false;
}

// Atomic increment using CAS
void atomic_increment(int *counter) {
    int old_val;
    do {
        old_val = *counter;
    } while (!compare_and_swap(counter, old_val, old_val + 1));
    // Retry until successful
}

// Mutual exclusion using CAS
int lock = 0;

void acquire() {
    while (!compare_and_swap(&lock, 0, 1))
        ;  // Change lock from 0 (available) to 1 (in use)
}

void release() {
    lock = 0;
}

Atomic Variables

Based on hardware instructions like CAS, these provide atomic operations on basic data types.

// C11 atomic variables
#include <stdatomic.h>

atomic_int counter = 0;

void increment() {
    atomic_fetch_add(&counter, 1);  // Atomic increment
}

// Java atomic variables
// import java.util.concurrent.atomic.AtomicInteger;
// AtomicInteger counter = new AtomicInteger(0);
// counter.incrementAndGet();  // Atomic increment

Atomic variables solve race conditions for single variables but are not a general solution for complex critical section problems.


Mutex Lock

A mutex is the simplest synchronization tool that protects a critical section like a lock.

// Mutex usage pattern
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void critical_section() {
    pthread_mutex_lock(&mutex);     // Acquire lock

    // Critical section
    // Access shared data

    pthread_mutex_unlock(&mutex);   // Release lock
}
[Mutex Operation]

Thread A                   Thread B
   |                          |
   |-- lock() success --      |
   |   (enter critical)       |-- lock() attempt
   |   access shared data     |   (waiting/blocked)
   |                          |   ...
   |-- unlock() ---------     |
   |                    ----->|-- lock() success
   |                          |   (enter critical)
   |                          |   access shared data
   |                          |-- unlock()

Spinlock: Repeatedly checks until the lock is obtained (busy waiting). Suitable for short critical sections on multicore systems because it avoids context switch overhead.

// Spinlock implementation (CAS-based)
typedef struct {
    atomic_flag flag;
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (atomic_flag_test_and_set(&lock->flag))
        ;  // Repeat until locked (busy waiting)
}

void spin_unlock(spinlock_t *lock) {
    atomic_flag_clear(&lock->flag);
}

Semaphore

A semaphore is a more general synchronization tool than a mutex. It is an integer variable that only allows wait() and signal() operations.

// Basic semaphore operations (atomic)
wait(S) {          // P operation or acquire
    while (S <= 0)
        ;          // Wait
    S--;
}

signal(S) {        // V operation or release
    S++;
}

Types of Semaphores

  • Counting Semaphore: No limit on value range. Tracks the number of available resources
  • Binary Semaphore: Only 0 or 1. Similar to a mutex
// Counting semaphore usage: Maximum 5 concurrent connections
#include <semaphore.h>

sem_t connection_pool;

void init() {
    sem_init(&connection_pool, 0, 5);  // Initial value 5
}

void handle_request() {
    sem_wait(&connection_pool);    // Acquire connection (decrease count)

    // Perform database operation
    // ...

    sem_post(&connection_pool);    // Return connection (increase count)
}

Order Control with Semaphores

// Guarantee S1 executes before S2
sem_t sync_sem;
sem_init(&sync_sem, 0, 0);  // Initial value 0

// Process 1
void process1() {
    S1;                     // Statement to execute first
    sem_post(&sync_sem);    // signal
}

// Process 2
void process2() {
    sem_wait(&sync_sem);    // wait (wait until S1 completes)
    S2;                     // Execute after S1
}

Semaphore Without Busy Waiting

In actual implementations, a waiting queue is used instead of busy waiting.

typedef struct {
    int value;
    struct list_head waiting_list;  // List of waiting processes
} semaphore;

void wait(semaphore *S) {
    S->value--;
    if (S->value < 0) {
        // Add current process to waiting list
        add_to_list(&S->waiting_list, current_process);
        block();  // Switch process to waiting state
    }
}

void signal(semaphore *S) {
    S->value++;
    if (S->value <= 0) {
        // Wake up one process from the waiting list
        process *P = remove_from_list(&S->waiting_list);
        wakeup(P);  // Switch P to ready state
    }
}

Monitor

Semaphores are powerful but difficult to use correctly and error-prone. A monitor is a high-level construct that supports synchronization at the programming language level.

[Monitor Structure]

+------------------------------------+
|           monitor                  |
|                                    |
|  Shared Data                       |
|  +-----------+                     |
|  | Variables  |                     |
|  +-----------+                     |
|                                    |
|  Condition Variables               |
|  condition x, y;                   |
|                                    |
|  procedure P1() { ... }           |
|  procedure P2() { ... }           |
|  procedure P3() { ... }           |
|                                    |
|  Initialization code               |
|                                    |
+------------------------------------+
      ^          ^          ^
      |          |          |
   Thread1    Thread2    Thread3
   (only one at a time inside the monitor)

Condition Variable

Used to wait within a monitor until a specific condition is met.

  • x.wait(): Puts the calling process in the condition x wait queue and releases the monitor
  • x.signal(): Wakes up one process from the condition x wait queue
// Monitor pattern in Java (synchronized keyword)
public class BoundedBuffer {
    private final Object[] buffer;
    private int count = 0;
    private int in = 0;
    private int out = 0;

    public BoundedBuffer(int size) {
        buffer = new Object[size];
    }

    // synchronized: Only one thread can execute at a time
    public synchronized void produce(Object item)
            throws InterruptedException {
        while (count == buffer.length) {
            wait();  // Wait if buffer is full
        }

        buffer[in] = item;
        in = (in + 1) % buffer.length;
        count++;

        notifyAll();  // Wake up waiting consumers
    }

    public synchronized Object consume()
            throws InterruptedException {
        while (count == 0) {
            wait();  // Wait if buffer is empty
        }

        Object item = buffer[out];
        out = (out + 1) % buffer.length;
        count--;

        notifyAll();  // Wake up waiting producers
        return item;
    }
}
// Pthreads condition variable usage
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

// Waiting thread
void *waiter(void *arg) {
    pthread_mutex_lock(&mutex);

    while (!ready) {
        // Wait until condition is met
        // Releases mutex while waiting, reacquires on wakeup
        pthread_cond_wait(&cond, &mutex);
    }

    printf("Condition met! Proceeding.\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// Signaling thread
void *signaler(void *arg) {
    pthread_mutex_lock(&mutex);

    ready = 1;
    pthread_cond_signal(&cond);  // Wake up waiting thread

    pthread_mutex_unlock(&mutex);
    return NULL;
}

Liveness Problems

Improper use of synchronization tools can cause processes to never make progress.

Deadlock

A state where two or more processes are each waiting for a resource held by the other, waiting forever.

[Deadlock Example]

Thread A                    Thread B
lock(mutex1);              lock(mutex2);
  // holds mutex1              // holds mutex2
lock(mutex2);              lock(mutex1);
  // waits for mutex2!         // waits for mutex1!
  // Each waits for the other's resource -> deadlock

Solution: Always acquire resources in the same order
Thread A                    Thread B
lock(mutex1);              lock(mutex1);  // Same order
lock(mutex2);              lock(mutex2);

Starvation

A state where a process can never obtain the resources it needs. For example, a low-priority process is continually preempted by higher-priority processes.

Priority Inversion

A situation where a high-priority process waits for a resource held by a low-priority process.

[Priority Inversion Scenario]

Priority: H > M > L

1. L acquires resource R and is running
2. H arrives, requests R -> waits because L holds R
3. M arrives, preempts L -> M runs
4. H needs L to release R, but L is preempted by M
5. Result: M runs before H! (priority inversion)

Solution: Priority inheritance protocol
L inherits H's priority while holding R
-> L is not preempted by M -> L releases R quickly

The Mars Pathfinder mission famously experienced a priority inversion bug. It was resolved by enabling the priority inheritance protocol.


Summary

Process synchronization is essential for resolving race conditions on shared resources. Various tools exist from hardware-level CAS and atomic variables to mutex and semaphore, and high-level monitors. If these tools are not used correctly, liveness problems such as deadlock, starvation, and priority inversion can occur.