데드락 예방과 회피
교착 상태의 필요조건을 깨는 예방 전략과 안전 상태를 유지하는 은행원 알고리즘의 판단 과정을 비교합니다.
데드락의 네 가지 필요조건을 알았으니, 이제 데드락이 발생하지 않도록 막는 방법을 살펴봅니다.
크게 예방(Prevention)과 회피(Avoidance) 두 가지 접근법이 있습니다.
예방은 필요조건 자체를 깨뜨리는 것이고, 회피는 조건은 허용하되 위험한 상태로 진입하지 않겠다고 런타임에서 판단하는 것입니다.
데드락 예방: 4가지 조건 깨뜨리기
예방은 네 가지 필요조건 중 최소 하나를 원천적으로 불가능하게 만드는 전략입니다.
각 조건을 어떻게 깨뜨릴 수 있는지, 그리고 왜 실무에서 쉽지 않은지를 함께 살펴봅니다.
1. 상호 배제 깨뜨리기
자원을 공유 가능하게 만들면 됩니다.
읽기 전용 파일, 불변(immutable) 데이터 구조는 동시 접근이 가능하므로 상호 배제가 필요 없습니다.
하지만 뮤텍스, 프린터, 쓰기 작업처럼 본질적으로 공유가 불가능한 자원이 있습니다.
프린터로 두 작업을 동시에 출력하면 결과물이 섞입니다.
따라서 모든 경우에 적용할 수는 없습니다.
실무 대안으로 스풀링(Spooling)이 있습니다.
프린터에 직접 접근하는 대신, 인쇄 작업을 큐에 넣고 프린터 데몬이 순서대로 처리합니다.
사실상 경쟁 자원을 큐로 변환하는 것입니다.
2. 점유와 대기 깨뜨리기
방법 A — 일괄 요청: 프로세스가 실행을 시작하기 전에 필요한 모든 자원을 한꺼번에 요청합니다.
중간에 추가 자원을 요청하지 않으므로 점유+대기 상태가 되지 않습니다.
방법 B — 선반납 후 요청: 새로운 자원이 필요하면, 현재 보유한 모든 자원을 먼저 반납한 후 필요한 자원 전체를 다시 요청합니다.
두 방법 모두 심각한 단점이 있습니다.
- 자원 이용률 저하: 실행 초반에 잠깐 사용할 자원을 끝까지 잡고 있어야 합니다. 프린터는 마지막에만 필요한데 시작할 때부터 점유합니다.
- 기아 가능성: 많은 자원을 한꺼번에 요청하는 프로세스는 모든 자원이 동시에 사용 가능해질 때까지 무한 대기할 수 있습니다.
- 미래 예측 불가: 프로세스가 실행 전에 필요한 자원 전체를 알기 어렵습니다. 사용자 입력에 따라 필요한 자원이 달라질 수 있습니다.
3. 비선점 깨뜨리기
자원을 보유한 프로세스가 다른 자원을 요청했는데 즉시 할당받지 못하면, 현재 보유한 자원을 강제로 반납시킵니다.
적용 가능한 자원: CPU 레지스터, 메모리 페이지처럼 상태를 저장하고 복원할 수 있는 자원.
실제로 CPU 스케줄링의 선점형 스케줄러는 이 원리를 사용합니다.
적용 불가능한 자원: 프린터(중간에 빼앗으면 출력이 망가짐), 뮤텍스(임계 구역 중간에 빼앗으면 데이터 일관성 파괴), 네트워크 연결(중간에 끊기면 프로토콜 상태 손실).
4. 순환 대기 깨뜨리기
여러 락을 다루는 코드에서 적용할 수 있는 방법입니다.
모든 자원에 전역적 번호(순서)를 부여하고, 프로세스는 반드시 번호가 증가하는 순서로만 자원을 요청합니다.
/* 락에 순서를 부여: lock1 = 순서 1, lock2 = 순서 2 */
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void *thread_a(void *arg) {
pthread_mutex_lock(&lock1); /* 순서 1 먼저 */
pthread_mutex_lock(&lock2); /* 순서 2 다음 */
/* --- 작업 --- */
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
void *thread_b(void *arg) {
pthread_mutex_lock(&lock1); /* 순서 1 먼저 (lock2부터 잡지 않음!) */
pthread_mutex_lock(&lock2); /* 순서 2 다음 */
/* --- 작업 --- */
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}양쪽 스레드 모두 lock1 → lock2 순서로 잡기 때문에 순환이 형성될 수 없습니다.
증명: 자원 번호가 오름차순으로만 요청된다고 가정합니다.
P1이 R5를 보유하고 R8을 기다린다면, P1은 5보다 큰 번호만 요청합니다.
P2가 R8을 보유하고 있다면, P2는 8보다 큰 번호만 요청할 수 있습니다.
따라서 P2가 R5(번호 5)를 요청하는 것은 불가능하므로 순환이 형성되지 않습니다.
실무 적용: Linux 커널은 lockdep이라는 도구로 런타임에 락 획득 순서를 추적합니다.
lockdep은 락 클래스 사이의 의존 관계와 사용 규칙을 검사합니다. 모든 락에 고정 번호를 부여하는 구현과는 구분합니다.
실제 데드락이 발생하기 전에 잠재적 문제를 찾는 예방적 디버깅 도구입니다.
import threading
class OrderedLock:
"""오름차순 락 획득을 검사하는 예제"""
_thread_local = {}
def __init__(self, order):
self._lock = threading.Lock()
self._order = order
def acquire(self):
tid = threading.current_thread().ident
held = OrderedLock._thread_local.get(tid, [])
if held and held[-1] >= self._order:
raise RuntimeError(
f"락 순서 위반: {held[-1]} 이후에 {self._order} 획득 시도"
)
self._lock.acquire()
held.append(self._order)
OrderedLock._thread_local[tid] = held
def release(self):
tid = threading.current_thread().ident
OrderedLock._thread_local[tid].pop()
self._lock.release()이 예제는 각 락에 서로 다른 순서를 주고, 획득한 스레드가 반드시 역순으로 해제한다는 전제입니다. release()는 락의 정체성이나 해제 순서를 검사하지 않으므로 잘못된 사용까지 막는 완성된 락 라이브러리는 아닙니다.
OrderedLock의 정상 사용을 order=10인 A, order=20인 B로 추적해 봅시다. A 획득 뒤 기록은 [10], B 획득 뒤는 [10, 20]입니다. 이때 order=10을 다시 획득하려 하면 마지막 20 이상이 아닌 요청이므로 실제 잠금 획득 전에 RuntimeError가 납니다. B, 이어 A를 해제하면 기록은 [10], [] 순서로 줄어듭니다. 이 예제는 순서 값의 비교를 보여 주며 락 정체성·오류 정리·수명까지 검증하는 구현은 아닙니다.
독립 확인: A와 B를 정상 획득한 뒤 A.release()를 먼저 호출했습니다. 원 코드가 이를 순서 위반으로 거절하나요? 기록에 남는 값과 실제로 풀리는 락을 구별하세요.
풀이 확인
거절하지 않습니다. release는 해당 스레드 기록의 마지막 값 20을 pop하지만 실제로는 A의 내부 락을 풉니다. 기록은 [10]인데 실제로 보유한 락은 B(order=20)이어서 서로 어긋납니다. 따라서 본문의 서로 다른 순서·역순 해제 전제가 필요하며, 순서 비교가 존재한다는 이유로 잘못된 해제까지 검출한다고 볼 수 없습니다.
데드락 회피
회피는 예방보다 유연합니다.
네 가지 조건 자체는 허용하지만, 자원 요청 시마다 이 요청을 수락하면 데드락이 발생할 수 있는지를 런타임에 판단합니다.
위험하면 요청을 지연시킵니다.
안전 상태와 불안전 상태
안전 상태(Safe State): 각 프로세스의 선언된 최대 요구량을 고려했을 때, 모두 완료할 수 있는 순서가 하나 이상 존재하는 상태입니다.
이 완료 순서를 안전 순서(Safe Sequence)라고 부릅니다.
불안전 상태(Unsafe State): 안전 순서가 존재하지 않는 상태입니다.
불안전 상태가 반드시 데드락을 의미하지는 않지만, 데드락이 발생할 가능성이 있습니다.
데드락은 불안전 상태의 부분 집합입니다.
회피 전략은 시스템을 항상 안전 상태로 유지합니다.
자원 요청이 들어오면, 가상으로 할당해 보고 결과가 안전 상태인지 확인합니다.
안전하면 할당하고, 불안전하면 대기시킵니다.
은행원 알고리즘
은행원 알고리즘(Banker's Algorithm)은 다익스트라가 제안한 데드락 회피 알고리즘입니다.
은행이 고객에게 대출을 해 줄 때, 모든 고객의 최대 대출 요구를 충족할 수 있는 범위에서만 대출하는 것과 같은 원리입니다.
가용 자원이 0이어도 추가 자원 없이 완료할 프로세스가 있으면 자원을 돌려받을 수 있습니다. 잔고가 0이라는 사실만으로 데드락을 판정하지 않습니다.
필요한 자료 구조
- Available[m]: 현재 사용 가능한 각 자원 유형의 인스턴스 수 (벡터, m개 자원 유형)
- Max[n][m]: 각 프로세스의 최대 자원 요구량 (n개 프로세스 × m개 자원 유형)
- Allocation[n][m]: 현재 각 프로세스에 할당된 자원
- Need[n][m]: 각 프로세스가 앞으로 필요한 자원 ()
안전성 검사 알고리즘
def is_safe(available, max_need, allocation, n_proc, n_res):
"""은행원 알고리즘: 안전 상태 여부 판정"""
need = [[max_need[i][j] - allocation[i][j]
for j in range(n_res)] for i in range(n_proc)]
work = available[:] # 현재 가용 자원 복사
finish = [False] * n_proc # 완료 여부
safe_seq = []
while len(safe_seq) < n_proc:
found = False
for i in range(n_proc):
if not finish[i]:
# Need[i] <= Work 인지 확인
if all(need[i][j] <= work[j] for j in range(n_res)):
# 프로세스 i가 완료 가능 → 자원 반환
for j in range(n_res):
work[j] += allocation[i][j]
finish[i] = True
safe_seq.append(i)
found = True
if not found:
return False, [] # 안전 순서 없음 → 불안전
return True, safe_seq
# 예제: 5개 프로세스, 3종류 자원 (A, B, C)
available = [3, 3, 2]
max_need = [
[7, 5, 3], # P0
[3, 2, 2], # P1
[9, 0, 2], # P2
[2, 2, 2], # P3
[4, 3, 3], # P4
]
allocation = [
[0, 1, 0], # P0
[2, 0, 0], # P1
[3, 0, 2], # P2
[2, 1, 1], # P3
[0, 0, 2], # P4
]
safe, seq = is_safe(available, max_need, allocation, 5, 3)
print(f"Safe: {safe}") # True
print(f"Sequence: {seq}") # [1, 3, 4, 0, 2]동작 과정 추적
다음 표는 코드의 입력을 손으로 추적한 결과입니다. 각 벡터는 A, B, C 순서이고 초기 Work는 [3, 3, 2]입니다.
은행원 알고리즘 · 미래 최대 요구량
남은 최대 요구량은 Need = Max − Allocation입니다
본문의 다섯 프로세스·세 자원(A, B, C) 입력을 사용합니다. 벡터의 뺄셈은 각 성분별이며 Need는 현재 Request와 다른 값입니다.
| 프로세스 | 최대 Max | 보유 Allocation | 남은 Need |
|---|---|---|---|
| P0 | [7, 5, 3] | [0, 1, 0] | [7, 4, 3] |
| P1 | [3, 2, 2] | [2, 0, 0] | [1, 2, 2] |
| P2 | [9, 0, 2] | [3, 0, 2] | [6, 0, 0] |
| P3 | [2, 2, 2] | [2, 1, 1] | [0, 1, 1] |
| P4 | [4, 3, 3] | [0, 0, 2] | [4, 3, 1] |
- P0
- 최대 Max: [7, 5, 3]보유 Allocation: [0, 1, 0]남은 Need: [7, 4, 3]
- P1
- 최대 Max: [3, 2, 2]보유 Allocation: [2, 0, 0]남은 Need: [1, 2, 2]
- P2
- 최대 Max: [9, 0, 2]보유 Allocation: [3, 0, 2]남은 Need: [6, 0, 0]
- P3
- 최대 Max: [2, 2, 2]보유 Allocation: [2, 1, 1]남은 Need: [0, 1, 1]
- P4
- 최대 Max: [4, 3, 3]보유 Allocation: [0, 0, 2]남은 Need: [4, 3, 1]
Need는 최대치까지 추가로 필요할 수 있는 양입니다. 지금 즉시 요청한 양과는 다릅니다.
안전 순서에 따른 가용 자원 변화
Need가 Work 이하이다. 마지막 프로세스도 자원을 반환한다. 각 단계의 값과 전환 이유를 비교합니다.
안전성 검사 · 완료와 반환
P1 → P3 → P4 → P0 → P2로 모두 완료할 수 있습니다
초기 Work=[3, 3, 2]이며 벡터는 A, B, C 순서입니다. 각 Need 성분이 Work 이하이면 최대 요구를 충족하고 완료할 수 있다고 가정하여 기존 Allocation을 Work에 더합니다.
Need ≤ Work: 최대 요구를 충족한 뒤 반환화살표는 검사상 완료·반환; 실제 OS 실행순서가 아님
검사상 완료·반환의 추론실제 OS 실행순서가 아님
| 완료 순서 | Need ≤ 완료 전 Work | 반환 후 Work |
|---|---|---|
| 1단계 · P1 | [1, 2, 2] ≤ [3, 3, 2] | [5, 3, 2] |
| 2단계 · P3 | [0, 1, 1] ≤ [5, 3, 2] | [7, 4, 3] |
| 3단계 · P4 | [4, 3, 1] ≤ [7, 4, 3] | [7, 4, 5] |
| 4단계 · P0 | [7, 4, 3] ≤ [7, 4, 5] | [7, 5, 5] |
| 5단계 · P2 | [6, 0, 0] ≤ [7, 5, 5] | [10, 5, 7] |
- 1단계 · P1
- Need ≤ 완료 전 Work: [1, 2, 2] ≤ [3, 3, 2]반환 후 Work: [5, 3, 2]
- 2단계 · P3
- Need ≤ 완료 전 Work: [0, 1, 1] ≤ [5, 3, 2]반환 후 Work: [7, 4, 3]
- 3단계 · P4
- Need ≤ 완료 전 Work: [4, 3, 1] ≤ [7, 4, 3]반환 후 Work: [7, 4, 5]
- 4단계 · P0
- Need ≤ 완료 전 Work: [7, 4, 3] ≤ [7, 4, 5]반환 후 Work: [7, 5, 5]
- 5단계 · P2
- Need ≤ 완료 전 Work: [6, 0, 0] ≤ [7, 5, 5]반환 후 Work: [10, 5, 7]
P1 → P3 → P4 → P0 → P2는 완료 가능한 순서 하나입니다. 모든 실행 순서가 안전하다는 뜻은 아닙니다.
Work는 실제 실행 기록이 아닌 안전성 검사의 임시 가용량입니다. 예를 들어 P1 반환 후 [3,3,2]+[2,0,0]=[5,3,2]입니다. Need를 빼기만 하는 계산과 구분합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 P1 완료 추론부터 P2 완료 추론까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | Need를 현재 Work와 성분별로 비교해 충족할 수 있으면 기존 Allocation 반환분을 더한다. 여섯 노드는 초기와 다섯 완료 뒤 Work다. 원 수치와 비교 조건은 아래 표와 같다. P1 P3 P4 P0 P2가 하나의 안전 순서다. |
| 표의 값 | 1단계 · P1. Need ≤ 완료 전 Work: [1, 2, 2] ≤ [3, 3, 2]. 반환 후 Work: [5, 3, 2]. 2단계 · P3. Need ≤ 완료 전 Work: [0, 1, 1] ≤ [5, 3, 2]. 반환 후 Work: [7, 4, 3]. 3단계 · P4. Need ≤ 완료 전 Work: [4, 3, 1] ≤ [7, 4, 3]. 반환 후 Work: [7, 4, 5]. 4단계 · P0. Need ≤ 완료 전 Work: [7, 4, 3] ≤ [7, 4, 5]. 반환 후 Work: [7, 5, 5]. 5단계 · P2. Need ≤ 완료 전 Work: [6, 0, 0] ≤ [7, 5, 5]. 반환 후 Work: [10, 5, 7]. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| P1 완료 추론 Need가 Work 이하이다. |
|
|
| P3 완료 추론 반환된 자원으로 다음 작업의 Need를 충족한다. |
|
|
| P4 완료 추론 세 성분을 모두 검사한다. |
|
|
| P0 완료 추론 미래 최대 요구까지 충족 가능하다. |
|
|
| P2 완료 추론 마지막 프로세스도 자원을 반환한다. |
|
|
안전 순서에 따른 가용 자원 변화
Need가 Work 이하이다. 마지막 프로세스도 자원을 반환한다. 각 단계의 값과 전환 이유를 비교합니다.
안전성 검사 · 완료와 반환
P1 → P3 → P4 → P0 → P2로 모두 완료할 수 있습니다
초기 Work=[3, 3, 2]이며 벡터는 A, B, C 순서입니다. 각 Need 성분이 Work 이하이면 최대 요구를 충족하고 완료할 수 있다고 가정하여 기존 Allocation을 Work에 더합니다.
Need ≤ Work: 최대 요구를 충족한 뒤 반환화살표는 검사상 완료·반환; 실제 OS 실행순서가 아님
검사상 완료·반환의 추론실제 OS 실행순서가 아님
| 완료 순서 | Need ≤ 완료 전 Work | 반환 후 Work |
|---|---|---|
| 1단계 · P1 | [1, 2, 2] ≤ [3, 3, 2] | [5, 3, 2] |
| 2단계 · P3 | [0, 1, 1] ≤ [5, 3, 2] | [7, 4, 3] |
| 3단계 · P4 | [4, 3, 1] ≤ [7, 4, 3] | [7, 4, 5] |
| 4단계 · P0 | [7, 4, 3] ≤ [7, 4, 5] | [7, 5, 5] |
| 5단계 · P2 | [6, 0, 0] ≤ [7, 5, 5] | [10, 5, 7] |
- 1단계 · P1
- Need ≤ 완료 전 Work: [1, 2, 2] ≤ [3, 3, 2]반환 후 Work: [5, 3, 2]
- 2단계 · P3
- Need ≤ 완료 전 Work: [0, 1, 1] ≤ [5, 3, 2]반환 후 Work: [7, 4, 3]
- 3단계 · P4
- Need ≤ 완료 전 Work: [4, 3, 1] ≤ [7, 4, 3]반환 후 Work: [7, 4, 5]
- 4단계 · P0
- Need ≤ 완료 전 Work: [7, 4, 3] ≤ [7, 4, 5]반환 후 Work: [7, 5, 5]
- 5단계 · P2
- Need ≤ 완료 전 Work: [6, 0, 0] ≤ [7, 5, 5]반환 후 Work: [10, 5, 7]
P1 → P3 → P4 → P0 → P2는 완료 가능한 순서 하나입니다. 모든 실행 순서가 안전하다는 뜻은 아닙니다.
Work는 실제 실행 기록이 아닌 안전성 검사의 임시 가용량입니다. 예를 들어 P1 반환 후 [3,3,2]+[2,0,0]=[5,3,2]입니다. Need를 빼기만 하는 계산과 구분합니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 P1 완료 추론부터 P2 완료 추론까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | Need를 현재 Work와 성분별로 비교해 충족할 수 있으면 기존 Allocation 반환분을 더한다. 여섯 노드는 초기와 다섯 완료 뒤 Work다. 원 수치와 비교 조건은 아래 표와 같다. P1 P3 P4 P0 P2가 하나의 안전 순서다. |
| 표의 값 | 1단계 · P1. Need ≤ 완료 전 Work: [1, 2, 2] ≤ [3, 3, 2]. 반환 후 Work: [5, 3, 2]. 2단계 · P3. Need ≤ 완료 전 Work: [0, 1, 1] ≤ [5, 3, 2]. 반환 후 Work: [7, 4, 3]. 3단계 · P4. Need ≤ 완료 전 Work: [4, 3, 1] ≤ [7, 4, 3]. 반환 후 Work: [7, 4, 5]. 4단계 · P0. Need ≤ 완료 전 Work: [7, 4, 3] ≤ [7, 4, 5]. 반환 후 Work: [7, 5, 5]. 5단계 · P2. Need ≤ 완료 전 Work: [6, 0, 0] ≤ [7, 5, 5]. 반환 후 Work: [10, 5, 7]. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| P1 완료 추론 Need가 Work 이하이다. |
|
|
| P3 완료 추론 반환된 자원으로 다음 작업의 Need를 충족한다. |
|
|
| P4 완료 추론 세 성분을 모두 검사한다. |
|
|
| P0 완료 추론 미래 최대 요구까지 충족 가능하다. |
|
|
| P2 완료 추론 마지막 프로세스도 자원을 반환한다. |
|
|
첫 선택 P1에서 Need=[1,2,2]는 Work=[3,3,2] 이하입니다. 최대 요구를 채워 주는 가상 배분 직후에는 Work가 [2,1,0]이지만 P1이 기존 Allocation=[2,0,0]과 방금 받은 [1,2,2]를 함께 반환하면 [5,3,2]가 됩니다. 이를 식으로 줄이면 원 Work+기존 Allocation입니다. 그래서 안전성 코드의 work에는 Need를 빼 둔 채 넘어가지 않고 기존 Allocation만 더합니다. 이 계산은 필요한 자원을 받은 프로세스가 완료·반환할 수 있다는 모형의 추론입니다.
자원 요청을 실제로 받아들일지는 같은 입력을 복사해 시험합니다. P1의 요청 [1,0,2]는 Need=[1,2,2]와 Available=[3,3,2]에 각각 성분별로 들어갑니다. 임시 Available=[2,3,0], P1 Allocation=[3,0,2], P1 Need=[0,2,0]으로 검사하면 P1을 먼저 완료하여 Work=[5,3,2]를 만들 수 있고, 이어 P3·P4·P0·P2도 완료합니다. 원 코드가 반환하는 안전 순서 하나는 [1,3,4,0,2]입니다.
반면 같은 최초 상태에서 P4가 [3,3,0]을 요청하면 Need=[4,3,1]와 Available 안에 들어가지만 임시 Available=[0,0,2]가 됩니다. P0는 A·B·C, P1은 A·B, P2는 A, P3는 B, 변경된 P4 Need=[1,0,1]은 A가 부족하여 첫 완료를 고를 수 없습니다. 현재 가용량에 들어가는 요청이어도 안전성 검사를 통과하는 것은 아닙니다. 이 요청은 임시 Available·Allocation을 최초 상태로 되돌리고 기다리게 합니다. 미래 최대치까지 고려한 불안전 판정이며, 현재 요청만으로 이미 교착됐다고 단정한 결과는 아닙니다.
자원 요청 알고리즘
프로세스 Pi가 Request[i]를 요청하면:
Request[i] > Need[i]인 성분이 있으면 선언한 최대 요구량을 넘긴 오류입니다.Request[i] > Available인 성분이 있으면 현재 자원이 부족하므로 기다립니다.- 가용량과 할당량을 임시로 바꾼 뒤 안전성 검사를 실행합니다.
- 안전하면 확정하고, 불안전하면 임시 변경을 되돌린 뒤 기다립니다.
독립 확인: 최초 입력에서 P0가 [0,2,0]을 요청합니다. 먼저 Need·Available과 비교하고 임시 Available·P0 Allocation·P0 Need를 계산한 뒤, 원 is_safe의 P0→P4 검사 순서로 안전 순서를 구하세요.
풀이 확인
[0,2,0]은 P0 Need=[7,4,3]와 Available=[3,3,2] 안에 들어갑니다. 임시 Available=[3,1,2], P0 Allocation=[0,3,0], P0 Need=[7,2,3]입니다. 첫 순회에서 P1은 B가 부족하지만 P3가 완료하여 Work=[5,2,3]이 됩니다. 다음 순회는 P0의 A가 아직 부족하므로 P1을 완료해 [7,2,3], P2를 완료해 [10,2,5]로 만듭니다. P4의 B=3은 아직 부족합니다. 다음 순회에서 P0가 완료하여 [10,5,5], P4가 완료하여 [10,5,7]이 됩니다. 코드 순서의 결과는 [3,1,2,0,4]이고 요청을 수락할 수 있습니다. 다른 안전 순서도 존재할 수 있으며, 원 입력이나 호출자의 배열을 시험 도중 덮어쓰면 롤백 비교가 깨집니다.
실무에서의 적용
은행원 알고리즘을 적용하려면 선언한 최대 요구량과 자원 반환 모델이 실제 시스템에 맞아야 합니다.
- 최대 요구량 예측 불가: 프로세스가 최대 자원 요구량을 미리 알려야 하는데, 사용자 입력이나 런타임 조건에 따라 달라집니다.
- 상태 관리: 프로세스가 추가·종료될 때 최대 요구량과 할당 상태를 일관되게 갱신해야 합니다.
- 계산 오버헤드: 매 요청마다 검사가 필요합니다.
락 순서 규칙, 타임아웃, try-lock은 상황에 따라 조합할 수 있습니다. 다음 조각은 두 번째 락을 비블로킹으로 시도하는 예입니다.
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void *safe_worker(void *arg) {
int retries = 0;
while (retries < 5) {
pthread_mutex_lock(&lock1);
if (pthread_mutex_trylock(&lock2) == 0) {
/* 두 락 모두 획득 성공 */
printf("작업 수행\n");
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
/* lock2 획득 실패 → lock1 반납하고 재시도 */
pthread_mutex_unlock(&lock1);
usleep(100 * (1 << retries)); /* 지수 백오프 */
retries++;
}
printf("락 획득 실패, 대체 로직 실행\n");
return NULL;
}pthread_mutex_trylock은 락 획득에 실패하면 블로킹하지 않고 즉시 반환합니다.
이 코드에서 재시도 상한은 lock2 획득 실패를 다섯 번까지 세는 것입니다. 첫 pthread_mutex_lock(&lock1)에는 시간 제한이 없으므로 함수 전체의 종료 시각은 보장하지 않습니다.
자원을 반납하고 간격을 늘리면 충돌 반복을 줄일 수 있지만, 고정된 백오프만으로 공정성이나 성공을 보장하지는 않습니다. 마지막 줄은 실패 안내만 출력하며 실제 대체 작업은 구현하지 않았습니다.
다음 절에서는 데드락이 발생한 후 이를 탐지하고 복구하는 방법을 알아보겠습니다.