운영체제 · 8장

교착 상태 Deadlocks

여러 스레드가 서로가 쥔 자원을 기다리며 영원히 멈춘다. 이 한 문장 뒤에는 “데드락이 성립하는 정확한 네 조건”, “안전 상태라는 미묘한 개념”, 그리고 은행원 알고리즘의 행렬 계산과, 실무에서 락 순서화·타임아웃으로 이를 피하는 기술이 숨어 있다.
기반: Operating System Concepts 10판 8장 · 대학원 수준으로 확장 · 이론 + 손계산 예제 + 다이어그램

이 장을 마치면

  • 자원의 요청·사용·해제 사이클과 자원 유형/인스턴스를 구분하고, 데드락이 “집합 내 모두가 집합 내 누군가만 일으킬 수 있는 사건을 기다리는” 상태임을 정의할 수 있다.
  • 두 뮤텍스 교차 획득으로 데드락을 코드로 재현하고, 같은 패턴이 라이브락(livelock)으로도 변질됨을 설명할 수 있다.
  • 데드락의 네 가지 필요조건을 말하고, 자원 할당 그래프의 사이클과 데드락의 관계(단일/다중 인스턴스)를 판별할 수 있다.
  • 데드락을 다루는 세 가지 전략(예방·회피 / 탐지·회복 / 무시)을 비교하고, 예방이 네 조건을 각각 어떻게 깨는지 설명한다.
  • 안전 상태·안전 순서를 정의하고, 은행원 알고리즘(안전성 + 자원요청)을 Available/Max/Allocation/Need 행렬로 직접 손계산할 수 있다.
  • wait-for graph(단일 인스턴스)와 다중 인스턴스 탐지 알고리즘을 적용하고, 탐지 빈도의 트레이드오프를 논증한다.
  • 데드락 회복(프로세스 종료·자원 선점·롤백)과 그때 생기는 기아(starvation) 문제를 다룰 수 있다.
🏷️ 출처 표시 — 이 페이지 읽는 법

이 자료는 교재(Operating System Concepts 10판 8장) 원문대학원 수준 확장을 더한 것입니다. 절·소절 제목 옆 배지로 출처를 구분했습니다.

📘 OSC 8.x 교재 8장 핵심 내용 ⊕ 교재 외 확장 교재 범위 밖 심화(실무 락 기법·우선순위 역전·분산 데드락·손계산 예제 등)

9절(실무와 연결)은 절 전체가 확장입니다. 배지 없는 소절은 교재 본문에 해당합니다. 일부 본문 절 안에도 ⊕ 표시된 확장 소절(손계산·구현 코드 등)이 섞여 있습니다.

6장의 락은 레이스 컨디션(race condition)을 막기 위한 도구였다. 그런데 그 도구를 잘못 쓰면 정반대의 병이 생긴다 — 아무도 레이스를 일으키지 않지만, 아무도 진행(progress)하지도 못한다. 모두가 서로를 기다리며 영원히 멈춘 상태, 이것이 교착 상태(deadlock)다. 7장의 식사하는 철학자들이 동시에 왼쪽 젓가락을 집는 순간이 바로 그 그림이다.

0학습 지도

주제핵심 질문
1시스템 모델 — 자원과 요청 사이클데드락은 정확히 어떤 “상태”인가?
2멀티스레드 앱의 데드락 · 라이브락두 뮤텍스만으로 어떻게 영원히 멈추나?
3데드락 특징 — 4조건 · 자원 할당 그래프그래프의 사이클은 항상 데드락인가?
4데드락 처리 방법 — 세 전략왜 Linux·Windows는 그냥 무시하나?
5데드락 예방 — 4조건 깨기실무에서 유일하게 쓸 만한 예방은?
6데드락 회피 — 안전 상태 · 은행원 알고리즘“가용한데도 거절”하는 게 말이 되나?
7데드락 탐지 — wait-for graph · 다중 인스턴스얼마나 자주 탐지를 돌려야 하나?
8데드락 회복 — 종료 · 선점 · 기아누구를 희생자로 죽일 것인가?
9실무와 연결 — 락 순서화 · 타임아웃 · 분산현장에선 실제로 무엇을 쓰나?
10오해 정리 · 요약 · 복습

1시스템 모델 — 자원과 요청 사이클 📘 OSC 8.1

시스템은 경쟁하는 여러 스레드에게 분배할 유한한 자원(resource)의 집합으로 이루어진다. 자원은 여러 유형(type, class)으로 나뉘고, 각 유형은 동일한 인스턴스(instance) 여러 개로 구성된다. 예를 들어 CPU가 4개면 자원 유형 “CPU”는 인스턴스가 4개다. 네트워크 인터페이스가 2개면 유형 “network”의 인스턴스가 2개다.

핵심 가정: 같은 유형의 인스턴스는 완전히 같다. 스레드가 어떤 유형의 인스턴스를 하나 요청하면, 그 유형의 어느 인스턴스를 줘도 요청이 충족돼야 한다. 만약 특정 인스턴스만 받아들여진다면, 그것은 인스턴스가 동일하지 않다는 뜻이고 자원 유형을 잘못 나눈 것이다.

🔑 락도 자원이다 — 현대 데드락의 가장 흔한 원천

6장의 뮤텍스 락·세마포어도 시스템 자원이며, 오늘날 데드락의 가장 흔한 출처다. 다만 락은 보통 특정 데이터 구조 하나와 1:1로 묶인다(이 락은 큐를 보호, 저 락은 연결 리스트를 보호…). 그래서 각 락 인스턴스는 보통 자기만의 자원 유형(class)으로 취급한다 — “뮤텍스”라는 한 유형의 인스턴스 여럿이 아니라, 락마다 별개 유형이다.

스레드가 커널 자원을 쓸 때는 반드시 다음 세 단계 사이클을 따른다. (다른 프로세스의 자원을 IPC로 쓰다 생기는 데드락도 있지만, 이는 커널의 관심사가 아니므로 이 장에서 다루지 않는다.)

① 요청 Request 못 받으면 대기(block) ② 사용 Use 임계 구역 진입 등 ③ 해제 Release 반납 → 큐의 다음에게 다시 다른 자원이 필요하면 요청부터 반복
자원 사용의 정상 사이클: 요청 → 사용 → 해제. 요청과 해제는 시스템 콜이다 — 장치의 request()/release(), 파일의 open()/close(), 세마포어의 wait()/signal(), 뮤텍스의 acquire()/release().

커널은 시스템 테이블에 각 자원이 free인지 allocated인지, 할당됐다면 어느 스레드에게 줬는지를 기록한다. 이미 다른 스레드가 쥔 자원을 요청하면, 요청한 스레드는 그 자원의 대기 큐에 들어간다.

심화 데드락의 형식적 정의

스레드 집합이 데드락 상태에 있다 ⇔ 그 집합의 모든 스레드가, 그 집합 안의 다른 스레드만이 일으킬 수 있는 사건을 기다린다. 여기서 “사건(event)”은 보통 자원의 획득과 해제다. 자원은 대개 논리적(뮤텍스·세마포어·파일)이지만, 네트워크 인터페이스 읽기나 IPC 같은 다른 사건도 데드락을 만들 수 있다. 정의의 핵심은 폐쇄성이다: 구원이 집합 바깥에서 오지 않는다. 외부의 누구도 이들을 풀어줄 수 없으므로 자력 탈출이 불가능하다.

2멀티스레드 앱에서의 데드락 · 라이브락 📘 OSC 8.2

이론으로 들어가기 전에, 두 개의 뮤텍스만으로 데드락이 어떻게 태어나는지 코드로 보자. POSIX에서 pthread_mutex_init()은 잠기지 않은 뮤텍스를 만들고, pthread_mutex_lock()은 획득(잠겨 있으면 소유자가 풀 때까지 블록), pthread_mutex_unlock()은 해제다.

교착의 씨앗 — 두 뮤텍스를 엇갈린 순서로 잡는다 (C, Pthreads · Figure 8.1)
pthread_mutex_t first_mutex;
pthread_mutex_t second_mutex;

pthread_mutex_init(&first_mutex,  NULL);
pthread_mutex_init(&second_mutex, NULL);

/* thread_one 이 실행하는 함수 */
void *do_work_one(void *param) {
    pthread_mutex_lock(&first_mutex);     /* (1) first  */
    pthread_mutex_lock(&second_mutex);    /* (2) second */
    /* ... 작업 ... */
    pthread_mutex_unlock(&second_mutex);
    pthread_mutex_unlock(&first_mutex);
    pthread_exit(0);
}

/* thread_two 가 실행하는 함수 — 순서가 거꾸로다 */
void *do_work_two(void *param) {
    pthread_mutex_lock(&second_mutex);    /* (1) second */
    pthread_mutex_lock(&first_mutex);     /* (2) first  */
    /* ... 작업 ... */
    pthread_mutex_unlock(&first_mutex);
    pthread_mutex_unlock(&second_mutex);
    pthread_exit(0);
}

thread_onefirst → second 순으로, thread_twosecond → first 순으로 잡는다. 데드락은 가능하다: thread_onefirst_mutex를 쥔 바로 그 순간 thread_twosecond_mutex를 쥐면, 각자 상대가 쥔 나머지 하나를 영원히 기다린다.

시간 ↓ · 점선 화살표 = "기다린다" thread_one lock(first) ✓ lock(second)대기 (blocked) thread_two lock(second) ✓ lock(first)대기 (blocked) 서로의 락을 기다림 → 영원히 멈춤
한 스레드는 first를 쥔 채 second를, 다른 스레드는 second를 쥔 채 first를 기다린다. 순환 대기가 닫히는 순간 둘 다 진행 불가. 데드락은 스케줄링에 따라 가끔만 나타나므로 테스트로 잡기 어렵다.
⚠️ 비결정성 — 데드락을 테스트로 못 잡는 이유

위 코드가 항상 데드락에 빠지는 건 아니다. thread_one이 두 락을 잡고 풀기까지 thread_two가 시작도 안 했다면 아무 문제 없이 끝난다. 어느 쪽이 먼저 도느냐는 CPU 스케줄러에 달려 있다. 그래서 데드락은 특정 스케줄링 상황에서만 터지는 비결정적(non-deterministic) 버그다 — 개발 환경에선 한 번도 안 나다가 프로덕션의 부하 아래서 처음 터진다.

2.1 라이브락(livelock) — 멈추지 않지만 진행도 없다

데드락과 사촌인 또 하나의 liveness 실패가 있다. 데드락은 “집합 내 모두가 블록되어” 멈추지만, 라이브락(livelock)은 스레드들이 블록되지 않고 계속 무언가를 시도하지만 그 시도가 매번 실패해서 진행이 없는 상태다. 복도에서 마주친 두 사람이 서로 같은 방향으로 비키기를 반복하며 길을 못 가는 상황과 같다.

라이브락 — trylock으로 다시 쓴 같은 예제 (C, Pthreads · Figure 8.2)
/* thread_one */
void *do_work_one(void *param) {
    int done = 0;
    while (!done) {
        pthread_mutex_lock(&first_mutex);
        if (pthread_mutex_trylock(&second_mutex)) {   /* 논블로킹 시도 */
            /* 둘 다 잡음 → 작업 */
            pthread_mutex_unlock(&second_mutex);
            pthread_mutex_unlock(&first_mutex);
            done = 1;
        } else {
            pthread_mutex_unlock(&first_mutex);       /* 실패 → 가진 것도 놓고 재시도 */
        }
    }
    pthread_exit(0);
}
/* thread_two 는 second → first 순서로 대칭 */

pthread_mutex_trylock()은 잠긴 락을 만나면 블록하지 않고 즉시 실패를 반환한다. 그래서 블록되지는 않는다. 그러나 thread_one이 first를, thread_two가 second를 잡은 뒤, 둘 다 나머지 하나에 trylock으로 실패하고, 가진 것을 놓고, 동시에 다시 시도하면 — 이 춤이 무한히 반복된다. 데드락처럼 진행이 없지만, CPU는 바쁘게 돌고 있다(그래서 더 음흉하다).

💡 해법 — 무작위 백오프(random backoff)

라이브락은 보통 “모두가 같은 타이밍에 실패한 연산을 재시도”할 때 생긴다. 각 스레드가 무작위로 다른 시간만큼 기다린 뒤 재시도하면 대칭이 깨진다. 정확히 이것이 이더넷이 충돌 시 쓰는 전략이다: 충돌 직후 즉시 재전송하지 않고 무작위 시간(backoff)을 둔 뒤 다시 보낸다. 라이브락은 데드락보다 드물지만, 역시 특정 스케줄링에서만 나타나 까다롭다.

3데드락 특징 — 네 가지 필요조건 · 자원 할당 그래프 📘 OSC 8.3

3.1 데드락의 네 가지 필요조건

데드락은 다음 네 조건이 동시에 성립할 때만 발생할 수 있다(필요조건). 하나라도 깨면 데드락은 불가능하다 — 5절의 예방이 바로 이 사실을 이용한다.

① 상호 배제 mutual exclusion 한 인스턴스는 한 번에 한 스레드만 ② 점유와 대기 hold and wait 하나 쥔 채로 다른 것을 기다림 ③ 비선점 no preemption 자발적 해제만 가능 강제로 못 뺏음 ④ 순환 대기 circular wait T0→T1→…→Tn →T0 의 대기 고리 네 조건 모두 성립해야 데드락 가능 · ④는 ②를 함의하므로 완전히 독립은 아니다
데드락의 네 필요조건. 예방(5절)은 이 중 적어도 하나가 성립하지 못하게 만든다. 실무에선 ④ 순환 대기를 깨는 것(자원 순서화)이 가장 현실적이다.
  1. 상호 배제(mutual exclusion): 적어도 하나의 자원이 비공유(nonsharable) 모드로 점유된다 — 한 번에 한 스레드만 그 인스턴스를 쓸 수 있다. 다른 스레드가 요청하면 해제될 때까지 지연된다.
  2. 점유와 대기(hold and wait): 어떤 스레드가 최소 하나의 자원을 쥔 채, 다른 스레드가 쥐고 있는 추가 자원을 기다린다.
  3. 비선점(no preemption): 자원은 선점(preemption — 강제로 빼앗음)될 수 없다. 점유한 스레드가 작업을 마치고 자발적으로만 해제한다.
  4. 순환 대기(circular wait): 대기 스레드 집합 {T0, T1, …, Tn}이 존재해서 T0은 T1이 쥔 자원을, T1은 T2가 쥔 자원을, …, Tn은 T0이 쥔 자원을 기다린다 — 고리가 닫힌다.

네 조건은 완전히 독립이 아니다: 순환 대기(④)는 점유와 대기(②)를 함의한다(고리 안의 각 스레드는 무언가를 쥔 채 기다리므로). 그럼에도 예방에서는 각 조건을 따로 공략하는 것이 유용하다(5절).

3.2 자원 할당 그래프(resource-allocation graph)

데드락은 방향 그래프로 정밀하게 그릴 수 있다. 정점은 두 종류다: 활성 스레드 T = {T1, …, Tn}(원으로 그림)와 자원 유형 R = {R1, …, Rm}(사각형으로 그림). 유형 Rj의 인스턴스 개수는 사각형 안의 점(dot) 개수로 표시한다. 간선은 두 종류다.

요청이 충족되면 요청 간선은 즉시 할당 간선으로 바뀌고, 자원을 해제하면 할당 간선이 삭제된다.

(a) 사이클 O · 데드락 O R1 R3 R2 T1 T2 T3 T3가 R2 요청(보라) → 고리 닫힘 → 셋 다 교착 (b) 사이클 O · 데드락 X R1 R2 T1 T2 T3 T4 T4가 R2를 곧 해제 → T3에게 줘 고리를 끊을 수 있다
(a) 단일 인스턴스 위주 그래프에 T3→R2 요청이 더해지면 두 개의 사이클이 닫혀 T1·T2·T3 모두 교착(Figure 8.5). (b) R1·R2가 인스턴스 2개씩이라 사이클이 있어도 T4가 R2를 해제하면 T3에게 할당돼 고리가 끊긴다 — 데드락 아님(Figure 8.6).
📐 사이클과 데드락의 정확한 관계

그래프에 사이클이 없으면 데드락은 없다(필요조건). 사이클이 있으면?

  • 모든 자원 유형이 인스턴스 1개라면, 사이클 ⇔ 데드락. 이때 사이클은 데드락의 필요충분조건이다.
  • 인스턴스가 여러 개인 유형이 끼면, 사이클은 데드락의 필요조건이지만 충분조건은 아니다. 고리 밖의 누군가가 인스턴스를 해제해 고리를 끊을 수 있기 때문이다(위 그림 b).

요약: 사이클 없음 → 안전. 사이클 있음 → 데드락일 수도, 아닐 수도.

4데드락을 다루는 세 가지 방법 📘 OSC 8.4

데드락 문제를 다루는 길은 크게 셋이다.

전략아이디어비용·특징쓰는 곳
예방 / 회피
(prevention / avoidance)
프로토콜로 시스템이 결코 데드락에 들지 않게 보장요청을 제약 → 자원 활용률·처리량 저하일부 임베디드·실시간·DB 일부
탐지 / 회복
(detection / recovery)
데드락을 허용하되, 주기적으로 탐지하고 회복탐지·회복 오버헤드데이터베이스(트랜잭션)
무시
(ostrich algorithm)
“데드락은 안 일어난다”고 가정하고 무시가장 쌈 · 드물면 합리적대부분의 OS — Linux·Windows
🦤 왜 “타조 알고리즘”인가 — 무시가 합리적인 이유

대부분의 OS(Linux·Windows 포함)는 세 번째를 택한다 — 모래에 머리를 박는 타조처럼 데드락을 못 본 척한다. 비용 때문이다. 데드락이 한 달에 한 번꼴로 드물게 난다면, 예방·탐지의 상시 오버헤드가 그 드문 사고보다 비쌀 수 있다. 데드락 처리는 커널·앱 개발자에게 맡겨진다(주로 예방 기법). 다만 탐지·회복이 없으면, 탐지되지 않은 데드락이 시스템 성능을 서서히 갉아먹다(자원을 쥔 채 못 도는 스레드가 늘어남) 결국 멈춰서 수동 재시작이 필요해진다.

예방과 회피는 모두 “데드락에 들지 않게 보장”하지만 방식이 다르다. 예방(prevention)은 네 필요조건 중 적어도 하나가 성립할 수 없게 요청 방식을 제약한다(5절). 회피(avoidance)는 각 스레드가 장차 어떤 자원을 얼마나 쓸지에 대한 추가 정보를 미리 받아, 매 요청마다 “지금 줘도 안전한가”를 동적으로 판단한다(6절). 한 시스템에서 자원 종류별로 서로 다른 전략을 조합하는 것이 최적일 때가 많다 — 어느 한 방법도 모든 자원 문제에 만능은 아니다.

5데드락 예방 — 네 조건을 각각 깨기 📘 OSC 8.5

데드락은 네 조건이 동시에 성립해야 가능하므로, 하나만 성립 못 하게 만들면 예방된다. 네 조건을 하나씩 공략해 보자.

깰 조건방법현실성
① 상호 배제공유 가능 자원만 쓰기(예: 읽기 전용 파일)대개 불가 — 뮤텍스는 본질적 비공유
② 점유와 대기시작 전 전부 요청 / 또는 “쥔 게 없을 때만 요청”활용률↓, 기아 위험
③ 비선점못 받으면 쥔 것을 모두 내려놓게(암묵 해제)락·세마포어엔 부적합
④ 순환 대기자원 전순서(total ordering) + 증가 순으로만 요청유일하게 실용적

5.1 상호 배제 깨기 — 대개 불가능

공유 가능(sharable) 자원은 상호 배제가 필요 없어 데드락에 끼지 않는다. 읽기 전용 파일이 좋은 예다 — 여러 스레드가 동시에 열어도 된다. 그러나 일반적으로 이 조건은 깰 수 없다. 어떤 자원은 본질적으로 비공유이기 때문이다 — 뮤텍스 락은 여러 스레드가 동시에 가질 수 없다.

5.2 점유와 대기 깨기

스레드가 자원을 요청할 때 다른 자원을 전혀 쥐고 있지 않도록 보장하면 된다. 두 프로토콜이 있다.

두 방법의 두 단점: ① 자원 활용률 저하 — 오래 쥐었는데 잠깐만 쓸 수 있다(뮤텍스를 전 실행 동안 쥐지만 실제론 잠깐만 필요). ② 기아(starvation) — 인기 있는 자원 여러 개가 필요한 스레드는 그중 하나가 늘 남에게 가 있어 무한정 못 받을 수 있다.

5.3 비선점 깨기

쥔 채로 다른 자원을 요청했는데 즉시 못 받으면, 현재 쥔 자원을 전부 (암묵) 해제한다 — 그 자원들은 “대기 목록”에 올라가고, 스레드는 옛 자원 + 새 자원을 모두 다시 얻을 수 있을 때 재시작된다. 또는, 요청한 자원이 대기 중인 다른 스레드에게 가 있으면 그것을 선점해 요청자에게 준다.

이 프로토콜은 상태를 쉽게 저장·복원할 수 있는 자원(CPU 레지스터, 데이터베이스 트랜잭션)에 잘 맞는다. 그러나 뮤텍스·세마포어처럼 — 정확히 데드락이 가장 흔히 나는 그 자원 — 에는 일반적으로 적용할 수 없다(임계 구역 중간에 락을 강제로 뺏으면 보호 대상 데이터의 일관성이 깨진다).

5.4 순환 대기 깨기 — 자원 순서화(유일하게 실용적)

앞의 셋은 대개 비현실적이다. 그러나 순환 대기를 깨는 것은 실용적이다. 모든 자원 유형에 전순서(total ordering)를 매기고, 각 스레드가 번호가 증가하는 순서로만 요청하게 하면 된다.

형식적으로, 자원 유형 집합 R = {R1, …, Rm}에 대해 일대일 함수 F: R → ℕ(자연수)을 정의한다. 예를 들어 Figure 8.1의 두 락에 F(first_mutex)=1, F(second_mutex)=5를 매긴다. 그러면 규칙은: Ri를 쥔 뒤에는 F(Rj) > F(Ri)인 Rj만 요청 가능. 두 락을 동시에 쓰려면 반드시 first 먼저, 그다음 second. (또는: Rj를 요청하려면 F(Ri) ≥ F(Rj)인 Ri는 모두 해제한 상태여야 한다.)

순서 없음 → 고리 닫힘 T1 first second T2 보라 = 요청(반대 방향) → 순환 F(first)=1 < F(second)=5 → 한 방향 T1 T2 first (1) second (5) 모두 first(1)→second(5) 순서 → 고리 못 닫음
자원 순서화: 모든 스레드가 번호 증가 순으로만 락을 잡으면, “더 작은 번호를 쥔 채 더 큰 번호를 기다리는” 한 방향만 생겨 순환이 불가능하다.
심화 왜 순서화가 순환을 막는가 — 모순에 의한 증명

순환 대기가 존재한다고 가정하자(귀류법). 고리의 스레드들을 {T0, …, Tn}, Ti는 Ti+1이 쥔 Ri를 기다린다고 하자(인덱스는 모듈로, Tn은 T0이 쥔 Rn을 기다림). Ti+1은 Ri를 쥔 채 Ri+1을 요청 중이므로, 규칙에 의해 모든 i에서 F(Ri) < F(Ri+1). 그러면 F(R0) < F(R1) < … < F(Rn) < F(R0), 추이성에 의해 F(R0) < F(R0) — 모순. 따라서 순환 대기는 존재할 수 없다. ∎

⚠️ 함정 — 동적으로 락을 얻으면 순서화도 무력

순서(계층)를 정해놓는 것만으로는 부족하다. 프로그램이 그 순서를 지켜 짜야 한다. 그리고 락을 동적으로 얻는 경우엔 순서화가 보장이 안 된다. 계좌 이체 예를 보자.

순서화가 깨지는 동적 락 — 계좌 이체 (의사코드 · Figure 8.7)
void transaction(Account from, Account to, double amount) {
    mutex lock1, lock2;
    lock1 = get_lock(from);     /* 인자에 따라 어느 락인지 런타임에 결정 */
    lock2 = get_lock(to);
    acquire(lock1);
        acquire(lock2);
            withdraw(from, amount);
            deposit(to, amount);
        release(lock2);
    release(lock1);
}

두 스레드가 인자를 뒤바꿔 동시에 호출하면 데드락이 난다: transaction(checking, savings, …)는 checking→savings, transaction(savings, checking, …)는 savings→checking 순으로 잡는다. 해법: from·to락의 식별값(예: 주소·해시)으로 정렬해 항상 작은 쪽부터 잡는다. Java 개발자들은 종종 System.identityHashCode(Object)를 순서 함수로 쓴다.

🛠️ Linux lockdep — 순서 위반을 자동 검출

락 순서를 지키는 것은 개발자 책임이지만, 도구가 검증을 도울 수 있다. Linux의 lockdep은 실행 중인 커널에서 락 획득·해제 패턴을 규칙과 대조해, 순서를 어긴 획득을 발견하면 “데드락 가능” 경고를 낸다. 인터럽트 핸들러에서도 쓰이는 스핀락을 인터럽트를 끄지 않고 잡는 위험한 패턴도 잡아낸다. lockdep은 시스템을 크게 느리게 하므로 프로덕션이 아니라 개발·테스트용이다(새 드라이버·커널 모듈이 데드락 원천을 만드는지 검사). 2006년 도입 후 몇 년 만에 시스템 보고된 데드락이 한 자릿수 배수로 줄었다고 한다. 최근 버전은 Pthreads 뮤텍스를 쓰는 사용자 앱의 데드락도 탐지할 수 있다.

6데드락 회피 — 안전 상태와 은행원 알고리즘 📘 OSC 8.6

예방은 요청을 제약해 부작용(낮은 장치 활용·처리량 감소)을 낳는다. 회피(avoidance)는 다른 길이다: 각 스레드가 장차 필요할 각 자원 유형의 최대 개수를 미리 선언하면, 시스템은 매 요청마다 “지금 줘도 미래에 데드락이 없는가”를 동적으로 검사해 결정한다. 판단에는 ① 현재 가용 자원, ② 각 스레드에 현재 할당된 자원, ③ 각 스레드의 최대 수요가 쓰인다.

6.1 안전 상태(safe state)와 안전 순서

상태가 안전(safe)하다는 것은: 시스템이 각 스레드에게 (최대 수요까지) 자원을 어떤 순서로 할당해도 데드락을 피할 수 있다는 뜻이다. 더 정확히, 안전 순서(safe sequence) ⟨T1, …, Tn⟩이 존재하면 안전하다 — 각 Ti아직 요청할 수 있는 자원이, 현재 가용 자원 + 자기보다 앞선 모든 Tj(j<i)가 쥔 자원으로 충족될 수 있어야 한다. 그러면 Ti는 (필요하면) 앞 스레드들이 끝나길 기다렸다가 자원을 얻어 끝내고 반납하고, 그다음 Ti+1이… 이렇게 줄줄이 완료된다.

전체 상태 (all states) unsafe (불안전) deadlock safe (안전) 안전 ⊂ 불안전 여집합 · 데드락 ⊂ 불안전 · 모든 불안전이 데드락은 아니다
상태 공간: 안전 상태는 데드락이 아니고, 데드락 상태는 반드시 불안전하다. 그러나 모든 불안전 상태가 데드락은 아니다 — 불안전은 “데드락으로 갈 수도 있는” 위험 지대다. 회피의 목표는 시스템을 항상 안전 영역에 두는 것.

6.2 안전/불안전 — 12개 자원 worked example

자원 12개, 스레드 셋(T0·T1·T2). 최대 수요와 현재 점유가 다음과 같다(t0 시점). 가용 = 12 − (5+2+2) = 3.

최대 수요 (Maximum)현재 점유 (Current)추가로 필요 (Need)
T01055
T1422
T2927
가용(Available)3

t0은 안전하다 — 안전 순서 ⟨T1, T0, T2⟩이 있다: 가용 3 ≥ T1의 Need 2 → T1에게 주고 끝내면 반납해 가용 5 → T0의 Need 5 충족 → 끝내면 가용 10 → T2의 Need 7 충족. 모두 완료.

이제 t1T2가 1개를 더 요청해 받았다고 하자(가용 3→2, T2 점유 2→3, Need 7→6). 시스템은 더 이상 안전하지 않다: 가용 2로는 오직 T1(Need 2)만 채울 수 있고, T1이 끝나면 가용 4. 그런데 T0는 Need 5, T2는 Need 6이라 둘 다 못 채운다 → 둘이 영원히 대기 → 데드락. 실수는 T2의 1개 추가 요청을 승인한 것이었다. 그 요청을 (가용했음에도) 대기시켰다면 데드락을 피했을 것이다.

💡 회피의 역설 — "가용한데도 거절"

회피 알고리즘의 핵심 규칙: 요청을 승인한 결과가 안전 상태일 때만 승인한다. 따라서 자원이 실제로 가용해도, 주면 불안전해진다면 스레드는 기다려야 한다. 자원 활용률이 떨어지는 대가로 데드락을 원천 차단하는 것이다.

6.3 자원 할당 그래프 알고리즘 — 단일 인스턴스용

모든 자원 유형이 인스턴스 1개면, 자원 할당 그래프의 변형으로 회피할 수 있다. 새 간선 요구 간선(claim edge) Ti → Rj(점선)를 도입한다 — “Ti가 장차 Rj를 요청할 수도 있다”. 스레드 시작 전 모든 claim 간선이 미리 그려져야 한다. Ti가 실제로 Rj를 요청하면 claim → request로, 할당되면 request → assignment로 바뀌고, 해제하면 다시 claim으로 돌아간다.

규칙: 요청 간선 Ti → Rj를 할당 간선 Rj → Ti로 바꿨을 때 그래프에 사이클(claim 간선 포함)이 생기지 않을 때만 승인한다. 사이클 검사는 n²(n=스레드 수) 연산이 든다. 사이클이 없으면 할당 후에도 안전, 있으면 불안전이므로 Ti는 대기한다.

claim(점선) 상태 — 아직 안전 R1 R2 T1 T2 실선=할당/요청 · 점선=claim(미래 요청 가능) T2가 R2 요청 → 사이클 → 불안전 R1 R2 T1 T2 보라 요청을 승인하면 고리 → 거절
회피용 자원 할당 그래프: T2가 R2를 요청하면(보라) 할당 간선으로 바꿨을 때 사이클이 생긴다 → 불안전 → 거절(Figure 8.9, 8.10). R2가 가용해도 미래 데드락을 막기 위해 T2를 대기시킨다.

6.4 은행원 알고리즘(Banker's Algorithm) — 다중 인스턴스용

그래프 알고리즘은 인스턴스 1개일 때만 쓴다. 유형마다 인스턴스가 여럿이면 은행원 알고리즘을 쓴다(이름의 유래: 은행이 모든 고객의 수요를 못 채울 만큼 현금을 내주지 않게 하는 것과 같다). 새 스레드는 진입 시 각 유형의 최대 필요 인스턴스 수를 선언한다. 요청이 오면 시스템은 “주면 안전한가”를 검사해, 안전하면 주고 아니면 기다리게 한다. n=스레드 수, m=유형 수일 때 네 자료구조를 유지한다.

자료구조형태의미
Available길이 m 벡터각 유형의 가용 인스턴스 수
Maxn×m 행렬각 스레드의 최대 수요
Allocationn×m 행렬각 스레드에 현재 할당된 수
Needn×m 행렬Max − Allocation (앞으로 더 필요한 수)

표기: 길이 n 벡터 X, Y에 대해 X ≤ Y ⇔ 모든 i에서 X[i] ≤ Y[i]. 행렬의 한 행을 벡터로 보고 Allocationi, Needi로 쓴다.

안전성 알고리즘 — 현재 상태가 안전한가? (의사코드, m×n² 연산)
// 1. 초기화
Work    = Available             // 길이 m
Finish[i] = false   for i = 0..n-1

// 2. 아직 못 끝났고 Need가 Work로 충족되는 스레드 찾기
loop:
  find i such that  Finish[i] == false  AND  Need[i] <= Work
  if none:  goto step4

// 3. 그 스레드가 끝났다 치고 자원을 회수(낙관적)
  Work    = Work + Allocation[i]
  Finish[i] = true
  goto loop

// 4. 판정
step4:
  if Finish[i] == true for ALL i:   상태는 SAFE (찾은 순서가 안전 순서)
  else:                             UNSAFE
자원요청 알고리즘 — Request_i 를 지금 줘도 되나? (의사코드)
// Ti 가 Request_i 를 요청
// 1. 최대 선언 초과 검사
if Request_i > Need_i:    error("최대 수요 초과")     // 규칙 위반

// 2. 가용 검사
if Request_i > Available: Ti must wait                // 지금은 자원 부족

// 3. "줬다 치고" 상태를 가상으로 갱신
Available     = Available     - Request_i
Allocation_i  = Allocation_i  + Request_i
Need_i        = Need_i        - Request_i

// 4. 안전성 알고리즘 실행
if 결과 상태가 SAFE:   요청 승인 (실제로 할당)
else:                  롤백(원상복구) 하고 Ti must wait

6.5 은행원 알고리즘 — 행렬 손계산 worked example ⊕ 확장

스레드 5개(T0~T4), 유형 셋 A(총 10)·B(총 5)·C(총 7). 현재 스냅샷:

Allocation (A B C)Max (A B C)Need = Max−Alloc (A B C)
T00 1 07 5 37 4 3
T12 0 03 2 21 2 2
T23 0 29 0 26 0 0
T32 1 12 2 20 1 1
T40 0 24 3 34 3 1
Available =3 3 2

현재 상태는 안전하다. 안전성 알고리즘을 Work=(3 3 2)로 돌려보자(각 단계: Need ≤ Work인 스레드를 골라 Allocation을 회수).

단계고른 스레드Need ≤ Work ?회수 후 Work
1T1(1 2 2) ≤ (3 3 2) ✓(3 3 2)+(2 0 0) = 5 3 2
2T3(0 1 1) ≤ (5 3 2) ✓+(2 1 1) = 7 4 3
3T4(4 3 1) ≤ (7 4 3) ✓+(0 0 2) = 7 4 5
4T2(6 0 0) ≤ (7 4 5) ✓+(3 0 2) = 10 4 7
5T0(7 4 3) ≤ (10 4 7) ✓+(0 1 0) = 10 5 7

모든 Finish[i]=true → 안전. 안전 순서 ⟨T1, T3, T4, T2, T0⟩.

심화 요청 검사 — Request₁ = (1, 0, 2)

이제 T1이 A 1개·C 2개를 추가 요청한다(Request1 = (1 0 2)). 자원요청 알고리즘:

  1. Request1=(1 0 2) ≤ Need1=(1 2 2) ✓ (최대 선언 안 넘음)
  2. Request1=(1 0 2) ≤ Available=(3 3 2) ✓ (가용함)
  3. “줬다 치고” 갱신: Available=(2 3 0), Allocation1=(3 0 2), Need1=(0 2 0)
  4. 안전성 검사 → 안전 순서 ⟨T1, T3, T4, T0, T2⟩ 존재 → 즉시 승인

반면 이 상태에서 T4의 (3 3 0) 요청은 가용(2 3 0)을 넘어 불가. T0의 (0 2 0) 요청은 가용한데도 결과가 불안전이라 거절된다 — 다시 “가용한데도 거절”의 사례다.

7데드락 탐지 — wait-for graph · 다중 인스턴스 📘 OSC 8.7

예방·회피를 안 쓰면 데드락이 일어날 수 있다. 이때 시스템은 ① 상태를 검사해 데드락 발생 여부를 판정하는 알고리즘과 ② 회복 알고리즘을 제공할 수 있다. 탐지·회복에는 오버헤드가 따른다 — 정보 유지·탐지 실행의 런타임 비용 + 회복에 따르는 손실.

7.1 단일 인스턴스 — wait-for graph

모든 자원이 인스턴스 1개면 wait-for graph(대기 그래프)를 쓴다. 자원 할당 그래프에서 자원 노드를 제거하고 간선을 합쳐 얻는다: wait-for 그래프에 Ti → Tj가 있다 ⇔ 원래 그래프에 어떤 자원 Rq에 대해 Ti → Rq → Tj가 있다(“Ti가 Tj가 쥔 자원을 기다린다”). 데드락 ⇔ wait-for 그래프에 사이클. 시스템은 이 그래프를 유지하며 주기적으로 사이클을 찾는다(O(n²), n=정점 수).

(a) 자원 할당 그래프 T5 R1 R3 R4 T1 T2 T3 T4 자원 제거 (b) wait-for 그래프 — 사이클! T5 T1 T2 T3 T4
wait-for 그래프는 자원 노드를 지우고 “Ti→Rq→Tj”를 “Ti→Tj”로 축약한 것(Figure 8.11). 보라색 T1·T2·T4가 이루는 사이클이 데드락을 드러낸다. 단일 인스턴스에서는 사이클이 곧 데드락.
🔧 BCC 툴킷 · Java thread dump

Linux의 BCC 툴킷은 pthread_mutex_lock/unlock 호출에 프로브를 끼워, 대상 프로세스의 뮤텍스 wait-for 그래프를 만들고 사이클이 보이면 데드락 가능성을 보고한다. Java는 명시적 데드락 탐지를 제공하진 않지만, thread dump(UNIX/Linux/macOS에서 Ctrl-\, Windows에서 Ctrl-Break)를 뜨면 JVM이 wait-for 그래프에서 사이클을 찾아 교착된 스레드를 보고한다 — 모든 스레드의 상태와 어떤 락을 기다리는지 스냅샷으로 보여준다.

7.2 다중 인스턴스 — 탐지 알고리즘

인스턴스가 여럿이면 wait-for 그래프를 못 쓴다. 은행원 알고리즘과 닮은 시변 자료구조(Available·Allocation·Request)를 쓴다. Request는 “지금 더 요청 중인 양”이다(Max·Need가 아니라 현재 요청). 알고리즘은 본질적으로 “완료될 수 있는 스레드를 낙관적으로 회수해 가며, 끝내 못 끝나는 스레드가 있으면 그것들이 데드락”임을 찾는다.

다중 인스턴스 탐지 알고리즘 (의사코드, m×n² 연산)
// 1. 초기화 — Allocation이 0이 아닌 스레드만 의심
Work = Available
for i = 0..n-1:
    Finish[i] = (Allocation[i] == 0)   // 아무것도 안 쥔 스레드는 데드락 아님

// 2. 끝낼 수 있는 스레드 찾기 (Need가 아니라 Request 사용!)
loop:
  find i such that  Finish[i] == false  AND  Request[i] <= Work
  if none:  goto step4

// 3. 낙관적 회수: Ti가 더 안 요청하고 곧 반납한다고 가정
  Work = Work + Allocation[i]
  Finish[i] = true
  goto loop

// 4. 판정
step4:
  if Finish[i] == false for some i:
        시스템은 DEADLOCK,  그런 Ti 들이 교착된 집합
심화 왜 step 2b에서 자원을 곧장 회수하나

Requesti ≤ Work이면 “Ti는 지금 데드락에 안 끼었다”고 본다. 그러면 낙관적으로 Ti가 더 요청 없이 곧 모든 자원을 반납하리라 가정하고 회수한다. 가정이 틀려 나중에 Ti가 더 요청하면 그때 데드락이 생길 수 있지만, 그것은 다음 번 탐지 실행에서 잡힌다. 이 낙관성 덕에 알고리즘이 “현재” 데드락만 정확히 집어낸다.

worked example: A(7)·B(2)·C(6), 스냅샷 Allocation T0=010,T1=200,T2=303,T3=211,T4=002, Request 모두 0이거나 작음, Available=000. 순서 ⟨T0,T2,T3,T1,T4⟩로 모두 Finish=true → 데드락 아님. 그런데 T2가 C 1개를 더 요청(Request2=001)하면, T0만 회수 가능하고 그 뒤 가용이 부족해 T1·T2·T3·T4가 못 끝남 → 이 넷이 데드락.

7.3 탐지를 얼마나 자주 돌릴까

탐지 빈도는 두 요인에 달렸다: ① 데드락이 얼마나 자주 나는가, ② 났을 때 몇 스레드가 엮이는가. 양극단을 보자.

🗄️ 데이터베이스의 탐지·회복

DB는 탐지·회복의 좋은 실례다. 트랜잭션은 여러 락을 쥐므로 동시 트랜잭션 사이에 데드락이 흔하다. 대부분의 트랜잭션 DB는 주기적으로 wait-for 그래프의 사이클을 찾아 데드락을 탐지하고, 희생자(victim) 트랜잭션을 골라 abort + rollback한다 — 그 트랜잭션의 락이 풀려 나머지가 진행한다. abort된 것은 나중에 재발행된다. 희생자 선택 기준은 DB마다 다르다(예: MySQL은 삽입·갱신·삭제 행 수가 최소인 트랜잭션을 고른다 — 되돌릴 일이 적으니까).

8데드락 회복 — 종료 · 선점 · 기아 📘 OSC 8.8

탐지가 데드락을 발견하면, 운영자에게 알려 수동 처리하게 하거나 시스템이 자동 회복한다. 고리를 깨는 길은 둘이다: 스레드(프로세스)를 종료하거나, 자원을 선점한다.

8.1 프로세스·스레드 종료

두 방식 모두 종료된 프로세스의 자원을 전부 회수한다.

종료는 간단하지 않다. 파일을 갱신 중이던 프로세스를 죽이면 파일이 부정합 상태로 남고, 뮤텍스로 공유 데이터를 갱신 중이었다면 시스템이 락 상태를 “가용”으로 되돌려도 데이터 무결성은 보장할 수 없다.

⚖️ "최소 비용" 희생자 고르기 — 정책 결정

부분 종료라면 누구를 죽일지 정해야 한다 — CPU 스케줄링과 비슷한 정책 결정이며 본질적으로 경제 문제다(“minimum cost”). 영향 요인: ① 프로세스 우선순위, ② 지금까지·앞으로의 계산 시간, ③ 사용한 자원의 수·종류(선점이 쉬운가), ④ 완료에 더 필요한 자원, ⑤ 몇 개를 죽여야 하는가.

8.2 자원 선점

교착 고리가 깨질 때까지 일부 자원을 프로세스에서 차례로 선점해 다른 프로세스에 준다. 세 이슈를 다뤄야 한다.

① 희생자 선택 어느 자원·프로세스를 선점? 비용 최소화 ② 롤백 안전 상태로 되돌려 재시작 (보통 total) ③ 기아 같은 놈만 계속 희생? 롤백 횟수를 비용에 포함 자원 선점 회복의 세 이슈
선점 기반 회복의 세 이슈. ②는 “안전 상태가 무엇인지 알기 어려워” 보통 전체 롤백(abort + restart)으로 단순화한다. ③ 기아는 “희생 횟수를 비용 함수에 넣어” 같은 프로세스가 무한정 희생되지 않게 막는다.
  1. 희생자 선택(selecting a victim): 어느 자원·프로세스를 선점할지. 종료와 마찬가지로 비용을 최소화하는 순서로(쥔 자원 수, 소비한 시간 등).
  2. 롤백(rollback): 자원을 뺏긴 프로세스는 정상 진행이 불가하므로 어떤 안전 상태로 되돌려 재시작해야 한다. 안전 상태를 알기 어려워 보통은 전체 롤백(abort 후 재시작)이 가장 단순하다. 고리를 깰 만큼만 부분 롤백하는 것이 효율적이지만, 그러려면 모든 실행 상태 정보를 더 많이 보관해야 한다.
  3. 기아(starvation): 비용 위주로만 희생자를 고르면 같은 프로세스가 늘 희생자가 되어 영영 못 끝날 수 있다. 실용 시스템은 이를 막아야 한다 — 한 프로세스가 희생자로 뽑히는 횟수에 유한한 상한을 둔다. 가장 흔한 해법은 롤백 횟수를 비용 함수에 포함해, 자주 당한 프로세스는 점점 덜 뽑히게 하는 것이다.

9실무와 연결 — 락 순서화 · 타임아웃 · 분산 데드락 ⊕ 교재 외 확장

교재는 알고리즘에 집중하지만, 실무 동시성 코드에서 데드락을 다루는 무기고는 더 구체적이다. 이 절은 앞 절들을 현장 기법·다른 장과 잇는다.

9.1 현장의 3대 무기 — 순서화 · tryLock · 타임아웃 ⊕ 확장

기법깨는 조건장점 / 단점
전역 락 순서화④ 순환 대기근본적·무오버헤드 / 모든 코드가 규율을 지켜야
tryLock + 백오프② 점유와 대기유연 / 라이브락 위험(무작위 백오프 필요)
락 타임아웃③ 비선점(스스로 포기)단순·실용 / 적절한 타임아웃 튜닝 어려움
락 계층(lock hierarchy)④ 순환 대기대형 코드베이스에 체계적 / 설계·문서화 필요
실무 — 주소로 정렬해 항상 같은 순서로 잡기 (C++)
void transfer(Account& a, Account& b, long amount) {
    // 두 락을 "주소가 작은 것 먼저" 고정 순서로 잡아 순환 대기 차단
    std::mutex& lo = (&a.m < &b.m) ? a.m : b.m;
    std::mutex& hi = (&a.m < &b.m) ? b.m : a.m;
    std::lock_guard<std::mutex> g1(lo);
    std::lock_guard<std::mutex> g2(hi);
    a.balance -= amount;
    b.balance += amount;
}

// 또는 표준 라이브러리가 데드락 없이 여러 락을 한꺼번에:
void transfer2(Account& a, Account& b, long amount) {
    std::scoped_lock lk(a.m, b.m);   // C++17: 내부적으로 deadlock-avoiding 알고리즘
    a.balance -= amount;
    b.balance += amount;
}
실무 — tryLock + 타임아웃으로 스스로 포기 (Java)
boolean transferWithTimeout(Account from, Account to, long amount)
        throws InterruptedException {
    long stop = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
    while (System.nanoTime() < stop) {
        if (from.lock.tryLock()) {
            try {
                if (to.lock.tryLock()) {          // 둘째 락도 논블로킹 시도
                    try {
                        from.balance -= amount;
                        to.balance   += amount;
                        return true;              // 성공
                    } finally { to.lock.unlock(); }
                }
            } finally { from.lock.unlock(); }     // 둘째 실패 → 첫째도 즉시 반납
        }
        Thread.sleep(ThreadLocalRandom.current().nextInt(1, 50)); // 무작위 백오프 — 라이브락 회피
    }
    return false;   // 타임아웃 → 포기(상위에서 재시도/보고)
}

9.2 6장·4장과의 연결 — 우선순위 역전, fork+락 ⊕ 확장

🔗 우선순위 역전(priority inversion) — 데드락은 아니지만 사촌

고우선 스레드 H가 저우선 스레드 L이 쥔 락을 기다리는데, 중간 우선순위 M이 L을 선점해 계속 돌면, L이 락을 못 풀어 H가 무한정 대기한다 — 우선순위 역전이다. 데드락(순환 대기)은 아니지만 같은 “진행 실패”다. 해법은 우선순위 상속(priority inheritance): 락을 쥔 L이 H의 높은 우선순위를 잠시 물려받아 빨리 끝내고 락을 푼다. (1997년 화성 탐사선 Mars Pathfinder의 리셋 반복이 바로 이 문제였고, 우선순위 상속 활성화로 해결됐다.) 6장의 동기화 도구가 이 메커니즘을 제공한다.

🍴 4장의 fork() + 락 — 자식이 상속하는 "주인 없는 잠긴 락"

멀티스레드 프로세스가 fork()하면 호출한 스레드만 자식에 복제된다. 그런데 다른 스레드가 그 순간 allocator 락을 쥐고 있었다면, 자식은 영원히 풀리지 않을 잠긴 락을 물려받는다(그 락을 쥐던 스레드는 자식에 없으니까). 자식이 malloc()을 부르면 그 락을 기다리며 데드락. 그래서 fork 후엔 exec()만 부르거나 pthread_atfork()로 락을 정돈한다. 4장 7절(스레딩 이슈)에서 상세히 다룬다.

9.3 분산 시스템의 데드락 ⊕ 확장

한 줄로: 자원과 프로세스가 여러 노드에 흩어진 분산 데드락에서는 어느 노드도 전역 wait-for 그래프를 못 본다 — 그래서 중앙 코디네이터가 부분 그래프를 모으거나(중앙집중식), 엣지 추적(edge-chasing) 같은 분산 탐지로 “probe 메시지”를 고리를 따라 돌려 자기에게 돌아오면 데드락으로 판정한다. 이는 운영체제를 넘어 분산 트랜잭션·마이크로서비스의 핵심 문제다.

10오해 정리 · 한 장 요약 · 복습 📘 OSC 8.9

10.1 흔한 오해 바로잡기

10.2 한 장 정리

🎯 8장 핵심

  • 데드락 = 집합 내 모든 스레드가 집합 내 누군가만 일으킬 사건을 기다리는 상태. 라이브락은 블록 없이 계속 실패만 반복(무작위 백오프로 해소).
  • 네 필요조건: ① 상호 배제 ② 점유와 대기 ③ 비선점 ④ 순환 대기. 넷이 동시에 성립해야 가능. ④는 ②를 함의.
  • 자원 할당 그래프: 사이클 없음 → 안전. 단일 인스턴스면 사이클 ⇔ 데드락, 다중이면 사이클은 필요조건일 뿐.
  • 세 전략: 예방/회피(결코 안 듦) · 탐지/회복(허용 후 처리, DB) · 무시(타조, 대부분 OS). 자원별 조합이 최적.
  • 예방은 네 조건을 각각 깬다. 셋은 비실용, 순환 대기를 자원 전순서로 깨는 것만 실용적(증명: F(R₀)<F(R₀) 모순).
  • 회피는 최대 수요를 미리 받아 안전 상태를 유지. 은행원 알고리즘: Available/Max/Allocation/Need로 안전 순서를 찾고(안전성), 요청을 “줬다 치고” 검사(자원요청). 가용해도 불안전이면 거절.
  • 탐지: 단일 인스턴스는 wait-for graph의 사이클, 다중은 Request 기반 알고리즘. 빈도는 데드락 빈도·CPU 활용률로 결정.
  • 회복: 프로세스 종료(전부/하나씩) 또는 자원 선점(희생자·롤백·기아). 같은 놈만 희생되지 않게 롤백 횟수를 비용에 포함.
  • 실무: 전역 락 순서화·주소 정렬·scoped_lock·tryLock+타임아웃+무작위 백오프. 사촌 문제로 우선순위 역전(상속으로 해결)·fork+락·분산 데드락.

10.3 복습 — 답을 가리고

Q1. 데드락의 네 필요조건을 말하고, 왜 “필요”조건이라 부르는지 설명하라.

① 상호 배제 ② 점유와 대기 ③ 비선점 ④ 순환 대기. 데드락이 일어나려면 넷이 동시에 성립해야 하므로 각각이 필요조건이다 — 뒤집으면 하나만 깨도 데드락은 불가능하다(예방의 원리).

Q2. 자원 할당 그래프에 사이클이 있다. 데드락이라고 단정할 수 있는가?

아니다. 모든 관련 자원 유형이 인스턴스 1개일 때만 사이클 ⇔ 데드락이다. 다중 인스턴스면 사이클은 필요조건일 뿐 — 고리 밖의 스레드가 인스턴스를 해제해 고리를 끊을 수 있다. 단, 사이클이 없으면 데드락은 확실히 없다.

Q3. 순환 대기를 자원 전순서로 깨면 왜 데드락이 불가능한가?

모든 스레드가 번호 증가 순으로만 잡으면, 고리가 있다고 가정 시 F(R₀)<F(R₁)<…<F(Rₙ)<F(R₀)이 되어 추이성으로 F(R₀)<F(R₀) — 모순. 따라서 순환이 닫힐 수 없다. 단, 개발자가 그 순서를 지켜 짜야 하고, 락을 동적으로 얻으면(계좌 이체) 식별값 정렬이 추가로 필요하다.

Q4. “자원이 가용한데도 요청을 거절”하는 회피 알고리즘. 이게 합리적인 이유는?

지금 주면 불안전 상태가 되어 미래에 데드락이 강제될 수 있기 때문이다. 회피는 “승인 결과가 안전 상태일 때만 승인”한다. 자원 활용률 저하를 대가로 데드락을 원천 차단한다(6.2의 12자원 예: T2의 추가 1개를 승인한 게 실수였다).

Q5. 은행원 알고리즘. Available=(3 3 2), Need가 T1=(1 2 2)·T3=(0 1 1)·T4=(4 3 1)·T2=(6 0 0)·T0=(7 4 3)일 때 안전 순서를 하나 제시하라.

Work=(3 3 2)에서 시작. T1 충족(≤Work)→회수 (5 3 2); T3→(7 4 3); T4→(7 4 5); T2→(10 4 7); T0→(10 5 7). 모두 완료 → 안전 순서 ⟨T1, T3, T4, T2, T0⟩.

Q6. wait-for graph는 자원 할당 그래프와 어떻게 다르며, 언제만 쓸 수 있나?

자원 노드를 지우고 “Tᵢ→Rq→Tⱼ”를 “Tᵢ→Tⱼ”로 축약한 것이다(Tᵢ가 Tⱼ가 쥔 자원을 기다림). 모든 자원이 인스턴스 1개일 때만 쓰며, 이때 사이클 ⇔ 데드락이다. 다중 인스턴스면 Request 기반 탐지 알고리즘을 써야 한다.

Q7. 선점으로 회복할 때 “기아”가 왜 생기며 어떻게 막나?

비용 위주로만 희생자를 고르면 같은 프로세스가 늘 희생자가 되어 영영 못 끝난다. 막는 법: 한 프로세스가 희생되는 횟수에 유한 상한을 두기 — 가장 흔한 구현은 롤백 횟수를 비용 함수에 포함해, 자주 당한 프로세스일수록 덜 뽑히게 한다.

Q8. 왜 대부분의 OS(Linux·Windows)는 데드락을 “무시”하나? 그 대가는?

예방·탐지의 상시 오버헤드보다, 드물게(예: 월 1회) 나는 데드락을 무시하는 게 더 싸기 때문이다(타조 알고리즘). 대가: 탐지되지 않은 데드락은 자원을 쥔 채 못 도는 스레드를 늘려 처리량을 갉아먹다 결국 멈춰, 수동 재시작이 필요해진다. 회피는 앱·커널 개발자 몫(락 순서화).

10.4 연관 자료 · 더 깊이

🚧 이 코스의 다른 장에 대해

본문은 같은 OS 코스의 6·7장과 “N절”을 자주 참조한다. 둘은 가리키는 곳이 다르다. “N절” 참조(예: 8.5절·8.6절)는 이 페이지 안의 앵커(#s5·#s6 등)로 이동한다. “N장” 참조 — 6장 동기화 도구·7장 동기화 예제 — 는 이 운영체제 코스에 순차적으로 추가되는 페이지를 가리킨다. 7장의 식사하는 철학자는 이 장 데드락의 전형이며, 6장의 락·세마포어는 데드락의 가장 흔한 자원이다. 그 페이지들이 준비되기 전까지, 동기화 기본기는 4장(스레드와 동시성)으로 보강하라.

심화 참고 문헌

Silberschatz·Galvin·Gagne, Operating System Concepts 10e, Ch.8 · Dijkstra, “Banker's algorithm”(EWD 원전) · Coffman·Elphick·Shoshani, “System Deadlocks”(1971, 네 조건의 고전) · Chandy·Misra·Haas, “Distributed Deadlock Detection”(edge-chasing) · Lampson·Redell, Mesa monitors(우선순위 역전·상속) · Linux lockdep 문서 · C++ std::scoped_lock / std::lock 레퍼런스.