컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 05

교착 상태

OS교착상태동기화

교착 상태(Deadlock)는 두 개 이상의 프로세스가 서로 상대방이 보유한 자원을 기다리며 영원히 진행되지 못하는 상태입니다. OS는 교착 상태를 예방·회피·탐지·복구 같은 전략으로 다루며, 교재에 따라 무시하는 방식까지 함께 분류합니다.

핵심 개념

교착 상태 4조건 (코프만)

상호 배제(Mutual Exclusion) · 점유와 대기(Hold and Wait) · 비선점(No Preemption) · 순환 대기(Circular Wait) — 네 조건이 동시에 성립할 때만 교착 상태가 발생합니다. 하나만 깨도 예방됩니다.

핵심 개념

자원 할당 그래프 (RAG)

프로세스(원)와 자원(사각형)의 관계를 방향 그래프로 표현합니다. 요청 간선: 프로세스→자원. 할당 간선: 자원→프로세스. 그래프에 사이클이 존재하면 교착 상태이거나 교착 상태 가능성이 있습니다(자원 종류마다 인스턴스가 하나이면 사이클이 곧 교착 상태이고, 여러 개이면 사이클은 필요조건일 뿐입니다).

핵심 개념

교착 상태 예방

4조건 중 하나를 원천 차단합니다. 순환 대기 예방이 실무에서 가장 흔합니다 — Lock 획득 순서를 전역적으로 통일합니다. 비선점 허용(우선순위 기반 선점)이나 점유와 대기 금지(모든 자원 한꺼번에 요청)도 방법입니다.

핵심 개념

은행원 알고리즘 (회피)

자원을 할당하기 전에 "안전 상태"인지 시뮬레이션합니다. 모든 프로세스가 순차적으로 완료될 수 있는 안전 순서가 존재하면 할당합니다. 최대 자원 요구량을 미리 선언해야 하는 단점이 있습니다.

핵심 개념

교착 상태 탐지 · 복구

주기적으로 자원 할당 그래프의 사이클을 탐지합니다. 복구 방법: 프로세스 종료(하나씩 또는 전체), 자원 선점(특정 프로세스에서 자원 강제 회수 후 롤백). 교착 상태가 드물다고 보고 대처하지 않는 방식을 Tanenbaum 교재는 타조 알고리즘(Ostrich Algorithm)이라 부릅니다.

핵심 개념

라이브락 · 기아

라이브락(Livelock)은 프로세스가 실행은 되지만 상태가 계속 바뀌면서 진행되지 못하는 상태입니다. 기아(Starvation)는 낮은 우선순위 프로세스가 계속 자원을 할당받지 못합니다. 둘 다 교착 상태는 아니지만 유사한 문제입니다.

상호 배제 — 자원을 한 번에 하나의 프로세스만 사용할 수 있습니다. 읽기 전용 자원은 공유 가능하므로 교착 상태 대상이 아닙니다.
점유와 대기 — 자원을 하나 이상 보유한 채 다른 자원을 추가 요청하며 대기합니다. 예방: 필요한 모든 자원을 실행 전에 한꺼번에 요청합니다.
비선점 — 프로세스가 자발적으로 반납하기 전까지 자원을 빼앗을 수 없습니다. CPU처럼 선점 가능한 자원은 교착 상태를 일으키기 어렵습니다.
순환 대기 — P1→R1→P2→R2→P1처럼 프로세스-자원 간 대기 사슬이 원을 이룹니다. 자원에 번호를 부여해 오름차순으로만 요청하면 순환이 불가능합니다.
현실에서의 교착 상태 — Linux 커널은 lockdep으로 Lock 획득 순서의 위반 가능성을 실행 중에 검증합니다. Java의 synchronized 중첩, 데이터베이스 트랜잭션 락 충돌이 대표적인 응용 교착 상태입니다.

교착 상태는 어떻게 생기나

락이나 자원을 하나 잡은 채 다른 것을 기다리는데, 서로가 상대가 가진 것을 기다리면 누구도 진행하지 못합니다. 교재는 상호 배제, 점유와 대기, 비선점, 순환 대기의 네 가지 조건이 모두 성립할 때 교착 상태가 생긴다고 설명하며, 이를 Coffman 조건이라고도 부릅니다(관련 문헌: Coffman 등, 'System Deadlocks', ACM Computing Surveys, 1971).

숫자로 확인하기

락 두 개의 교착 — 스레드 1이 A를 잡고 B를 기다리고, 스레드 2가 B를 잡고 A를 기다리면 둘 다 영원히 진행하지 못합니다. 두 스레드가 모두 A, B 순서로 잡도록 규칙을 정하면 이 사슬은 만들어지지 않습니다.
식사하는 철학자 5명 — 포크 5개에 철학자 5명이 모두 왼쪽 포크를 동시에 집으면 포크 5개가 모두 사용 중이라 남는 포크가 0개이고, 모두 오른쪽 포크를 기다리며 멈춥니다.
은행원 알고리즘: 안전 상태 — 자원이 총 10개이고 P0이 3개 보유(최대 7), P1이 2개 보유(최대 5), P2가 2개 보유(최대 9)이면 사용 가능한 자원은 10 − 7 = 3개입니다. 남은 요구량은 P0이 4, P1이 3, P2가 7입니다. P1(3 ≤ 3)이 끝나면 가용 5, P0(4 ≤ 5)이 끝나면 가용 8, P2(7 ≤ 8)가 끝나므로 안전 순서 <P1, P0, P2>가 있어 안전 상태입니다.
불안전 상태는 교착 상태가 아니다 — 위 상태에서 P2에게 2개를 더 주면 가용은 1개, 남은 요구량은 P0 4, P1 3, P2 5라서 아무도 끝까지 갈 수 없는 불안전 상태가 됩니다. 아직 교착 상태는 아니지만 교착 상태에 빠질 수 있어 회피 알고리즘은 이 할당을 미룹니다.

자주 하는 오해

오래 걸리면 교착 상태다 — 교착 상태는 외부 도움 없이는 영원히 풀리지 않는 대기이고, 단순히 느린 것과 다릅니다.
네 조건 중 하나만 성립해도 교착 상태가 생긴다 — 네 조건이 모두 성립해야 합니다. 그래서 하나만 깨도 예방할 수 있습니다.
불안전 상태이면 이미 교착 상태다 — 불안전 상태는 교착 상태가 될 가능성이 있는 상태이고, 이후 요청 순서에 따라 피할 수도 있습니다.
운영체제가 교착 상태를 알아서 풀어 준다 — 일반 응용의 락 대기는 보통 자동으로 풀리지 않습니다. 데이터베이스처럼 탐지와 해소를 직접 하는 시스템도 있습니다.

면접에서 이렇게 물어요

면접 질문

교착 상태의 발생 조건 네 가지는 무엇인가요?

상호 배제, 점유와 대기, 비선점, 순환 대기입니다. 네 조건이 동시에 성립할 때 발생하므로 하나를 깨면 예방됩니다.

면접 질문

예방, 회피, 탐지와 복구는 어떻게 다른가요?

예방은 네 조건 중 하나를 구조적으로 막는 것이고, 회피는 자원을 줄 때마다 안전한지 확인하는 것(은행원 알고리즘)입니다. 탐지와 복구는 일단 발생을 허용하고 사이클을 찾아 프로세스를 종료하거나 자원을 회수합니다.

면접 질문

실무에서 교착 상태를 어떻게 피하나요?

락을 잡는 순서를 전역으로 통일하고, 락을 잡은 채 오래 걸리는 작업이나 다른 락 요청을 피하며, tryLock에 타임아웃을 둡니다. 트랜잭션도 같은 순서로 자원에 접근하게 합니다.

면접 질문

교착 상태가 생겼는지 어떻게 알아내나요?

Java는 ThreadMXBean으로 교착 상태의 플랫폼 스레드를 찾을 수 있고, 데이터베이스는 대기 사슬을 탐지해 한 트랜잭션을 중단시킵니다.

실제 시스템에서는

PostgreSQL — PostgreSQL 문서는 교착 상태를 자동으로 탐지해 관련 트랜잭션 중 하나를 중단시킨다고 설명하고, 일반적으로 가장 좋은 방어는 모든 응용이 같은 순서로 락을 잡는 것이라고 안내합니다. 탐지를 시작하기까지 기다리는 시간은 deadlock_timeout이며 기본값이 1초입니다.
Java — ThreadMXBean.findDeadlockedThreads()는 락(object monitor, ownable synchronizer) 때문에 교착 상태에 빠진 플랫폼 스레드를 찾는 메서드이고, 가상 스레드는 대상이 아니라고 Javadoc이 설명합니다.
Linux 커널의 lockdep — 커널 문서는 lockdep이 락 사용 순서를 추적해 교착 상태 가능성을 실행 중에 알려 주는 검증 도구라고 설명합니다.

함께 보면 좋은 토픽

프로세스 동기화 — 교착 상태가 생기게 하는 락과 세마포어를 봅니다.
스레드 — 락을 잡고 경쟁하는 실행 단위를 봅니다.
CPU 스케줄링 — 기아와 에이징을 비교합니다.
락 / 동시성 — 데이터베이스에서 교착 상태가 생기는 방식을 봅니다.
그래프 — 자원 할당 그래프의 사이클 탐색과 이어집니다.

참고 자료

Java ThreadMXBean — ThreadMXBean — Java SE 21 API
논문·교재 — Coffman, Elphick, Shoshani, "System Deadlocks", ACM Computing Surveys, 1971 / Silberschatz, Galvin, Gagne, "Operating System Concepts", 10th Edition, "Deadlocks" 장

C — 교착 상태 발생 / Lock 순서 통일로 예방

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t r1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t r2 = PTHREAD_MUTEX_INITIALIZER;

/* ── 교착 상태 발생 버전 ─────────────────────────────────── */
void *thread1_bad(void *arg) {
    pthread_mutex_lock(&r1);   // R1 획득
    pthread_mutex_lock(&r2);   // R2 요청 → T2가 보유 시 블로킹
    /* 임계 구역 */
    pthread_mutex_unlock(&r2);
    pthread_mutex_unlock(&r1);
    return NULL;
}

void *thread2_bad(void *arg) {
    pthread_mutex_lock(&r2);   // R2 획득
    pthread_mutex_lock(&r1);   // R1 요청 → T1이 보유 시 블로킹 → 교착!
    /* 임계 구역 */
    pthread_mutex_unlock(&r1);
    pthread_mutex_unlock(&r2);
    return NULL;
}

/* ── 예방 버전: 항상 r1 → r2 순서로 획득 ───────────────────── */
void *thread1_safe(void *arg) {
    pthread_mutex_lock(&r1);   // 낮은 번호(r1) 먼저
    pthread_mutex_lock(&r2);   // 높은 번호(r2) 나중
    /* 임계 구역 — 순환 대기 불가 */
    pthread_mutex_unlock(&r2);
    pthread_mutex_unlock(&r1);
    return NULL;
}

void *thread2_safe(void *arg) {
    pthread_mutex_lock(&r1);   // T1·T2 모두 r1 먼저 → 순환 없음
    pthread_mutex_lock(&r2);
    /* 임계 구역 */
    pthread_mutex_unlock(&r2);
    pthread_mutex_unlock(&r1);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    /* safe 버전 실행 */
    pthread_create(&t1, NULL, thread1_safe, NULL);
    pthread_create(&t2, NULL, thread2_safe, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("완료 — 교착 상태 없음\n");
    return 0;
}

교착 상태는 Lock 획득 순서를 정반대로 할 때 발생합니다. T1이 r1→r2, T2가 r2→r1 순서로 획득하면 각자 첫 번째 Lock을 보유한 채 두 번째 Lock을 기다리는 순환 대기가 형성됩니다.
예방책: 모든 스레드에서 Lock 번호 오름차순(r1 → r2)으로만 획득합니다. 이렇게 하면 순환 대기 조건이 성립할 수 없어 교착 상태가 발생하지 않습니다.
pthread_mutex_trylock()은 Lock이 이미 잠겨 있으면 즉시 EBUSY로 실패하는 비블로킹(non-blocking) 함수입니다. 시간 제한 대기가 필요하면 pthread_mutex_timedlock()을 사용합니다. trylock 실패 시 보유한 Lock을 풀고 재시도하는 방식으로 교착을 회피할 수 있지만, 라이브락 가능성이 있으므로 재시도 간격에 지수 백오프(Exponential Backoff)를 적용해야 합니다.

Java — 교착 상태 + tryLock 타임아웃으로 회피

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockDemo {

    private static final ReentrantLock r1 = new ReentrantLock();
    private static final ReentrantLock r2 = new ReentrantLock();

    /* ── 교착 상태 발생 가능 버전 ──────────────────────────────── */
    static void badTask1() throws InterruptedException {
        r1.lock();
        Thread.sleep(50);  // T2가 r2를 잡을 시간 부여
        r2.lock();         // T2가 r2 보유 시 블로킹 → 교착 위험
        try { /* 작업 */ } finally { r2.unlock(); r1.unlock(); }
    }

    /* ── tryLock 타임아웃으로 회피 ─────────────────────────────── */
    static boolean safeTask(ReentrantLock first, ReentrantLock second)
            throws InterruptedException {

        if (!first.tryLock(100, TimeUnit.MILLISECONDS)) {
            return false;  // 100ms 내 획득 실패 → 포기
        }
        try {
            if (!second.tryLock(100, TimeUnit.MILLISECONDS)) {
                return false;  // 두 번째 획득 실패 → first도 자동 해제(finally)
            }
            try {
                /* 두 Lock 모두 보유 — 안전하게 작업 */
                System.out.println("작업 완료");
                return true;
            } finally {
                second.unlock();
            }
        } finally {
            first.unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            try {
                while (!safeTask(r1, r2)) {
                    Thread.sleep(10 + (long)(Math.random()*40)); // 지수 백오프 대체
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread t2 = new Thread(() -> {
            try {
                // 동일 순서(r1→r2)로 요청 — 순환 대기 예방
                while (!safeTask(r1, r2)) {
                    Thread.sleep(10 + (long)(Math.random()*40));
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        t1.start(); t2.start();
        t1.join();  t2.join();
    }
}

ReentrantLock.tryLock(timeout)은 지정 시간 내에 Lock을 획득하지 못하면 false를 반환합니다. 두 번째 Lock 획득에 실패하면 첫 번째 Lock도 finally 블록에서 즉시 해제되어 교착 상태를 회피합니다.
tryLock 방식은 라이브락 가능성이 있으므로 재시도 간격에 랜덤 지연(Jitter)을 추가해야 합니다. 두 스레드가 정확히 같은 간격으로 재시도하면 계속 충돌할 수 있습니다.
가장 안전한 방법은 Lock 획득 순서를 코드 레벨에서 강제(항상 낮은 번호 → 높은 번호)하는 것입니다.
Java의 System.identityHashCode()로 객체 ID 기준 정렬 후 획득하는 패턴이 실무에서 사용됩니다.

Q1

교착 상태(Deadlock)가 발생하기 위한 4가지 필요조건에 포함되지 않는 것은?

데드락 조건은 '비선점(no preemption)'입니다. 반대말을 찾아보세요.
데드락의 4가지 필요조건은 상호 배제, 점유와 대기, 비선점(선점 불가), 순환 대기입니다. "선점 가능"은 오히려 데드락을 막는 조건입니다.

Q2

자원 할당 그래프(RAG)에서 사이클이 존재한다는 것은?

서로가 서로를 기다리는 순환 구조입니다.
자원 할당 그래프에서 사이클이 발생하면(각 자원 종류가 1개씩인 경우) 데드락을 의미하며, 여러 개인 경우엔 데드락 가능성을 시사합니다.

Q3

데드락을 예방하는 방법으로 '점유와 대기' 조건을 깨는 방법은?

이미 자원을 가진 채로 다른 자원을 기다리지 못하게 합니다.
필요한 자원을 처음부터 한 번에 모두 요청하게 하면, 일부만 점유한 채 나머지를 기다리는 상황(점유와 대기)이 사라져 데드락을 예방할 수 있습니다.