💋 데드락이란?
2개 이상의 스레드가 서로가 가진 리소스를 기다리느라 무한히 대기하는 상태
💋 데드락의 발생 조건
데드락은 다음 네 가지 조건이 동시에 성립할 때 발생합니다.
✔️ Mutual Exclusion
- 프로세스끼리 자원 공유 X.
- 여기서 자원은 critical section, lock일 수도 있고, CPU, Memory, SSD, 프린터일 수도 있음.
✔️ Hold and Wait
- 프로세스가 이미 1개 이상의 리소스를 취득한 상태에서, 다른 프로세스가 사용중인 리소스를 추가로 기다린다.
- P1은 R3 리소스를 취득한 채로, P2가 사용중인 R1 리소스를 추가로 기다린다.
✔️ No Preemption
- 리소스 반환은, 오직 그 리소스를 취득하고 있는 프로세스만 할 수 있다.
- P1은 억지로 R1을 빼앗을 수 없고, P2가 R1을 반환할 때까지 기다려야 한다.
✔️ Circular Wait
- 프로세스들이 순환하는 형태로 서로의 리소스를 기다린다.
- 빙글빙글
데드락이 발생하면, 어떤 스레드도 일을 완수하지 못하고, 다 기다리고만 있다.
내 API 서버에서 일어나면 끔찍한데,,, 이제는 해결에 대해서 알아보자!
💋 OS의 데드락 해결 방법
크게는 4가지가 있지만, 미리 초치자면 이중 어느 것도 데드락을 완벽하게 해결해주지는 못한다.
- 데드락 방지
- 데드락 회피
- 데드락 감지와 복구
- 데드락 무시
✔️ 데드락 방지(Deadlock prevention)
위에서 언급한 데드락 발생 조건 4가지 중 1개가 충족되지 않도록 시스템 디자인
- Mutual Exclusion 금지!
- 리소스를 공유 가능하도록 설정!
- 현실적으로 불가능함 ㅠㅠ critical section도 다 필요가 있어서 만들어진거고 프린터, CPU도 모두 동시에 사용할 수는 없음.
- Hold And Wait 금지!
- 필요한 리소스를 모두 획득한 후에 시작하도록 설계
- 앞선 리소스에 걸리는 작업이 오래 걸리면, 후에 사용할 리소스가 오래 놀게 되어 리소스 사용 효율이 떨어짐.
- 리소스를 전혀 취득하지 않은 채로만 리소스를 요청하도록 설계
- 인기가 많은 리소스의 경우에 진짜 무한으로 대기할 수 있음. 스레드가 기아에 허덕여서 starvation이라고도 부름.
- 필요한 리소스를 모두 획득한 후에 시작하도록 설계
- No Preemption 금지!
- 추가적으로 리소스를 기다려야 하면, 다른 스레드가 리소스를 뺏어갈 수 있도록 한다. (CPU의 context switching 느낌)
- Circulat Wait 금지!
- 모든 리소스에 순서 체계를 부여해서 오름차순으로 리소스를 요청
- 4가지 중 가장 많이 사용되는 방식
✔️ 데드락 회피(Deadlock avoidance)
실행 환경에서 추가적인 정보(=현재 사용 가능한 리소스, 이미 사용중인 리소스 등)를 활용해 데드락이 발생할 것 같은 상황을 미리 예측해 회피
시스템 상태를 모니터링하고 데드락이 발생할 것 같으면 자원을 할당하지 않거나, 이미 할당된 자원을 해제한다.
데드락 회피에 사용되는 Banker Algorithm은 리소스 요청을 허락했을 때 데드락 발생 가능성이 있으면, 리소스를 할당해도 안전할 때까지 계속 요청을 거절하는 알고리즘이다.
✔️ 데드락 감지와 복구(Deadlock Detection and Recovery)
데드락을 허용하고, 데드락이 발생하면 복구
회복 작업은 데드락을 일으킨 프로세스 중 하나 혹은 여러 개를 중단시키거나, 일시적으로 자원을 선점하는 것을 허용해 데드락을 해결할 수 있다.
✔️ 데드락 무시(Do Nothing)
개발자가 알아서 하겠지
은근 많은 운영체제에서 이 방식을 선택한다.
💋 Deadlock in Java
✔️ 데드락 상황 연출
리소스인 lock1, lock2는 뮤텍스 락이므로 한 번에 하나의 스레드만 취득할 수 있다.
public class Main {
public static void main(String[] args) {
final Object lock1 = new Object();
final Object lock2 = new Object();
final Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread T1 get lock1");
synchronized (lock2) {
System.out.println("Thread T1 get lock2");
}
}
});
final Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread T2 get lock2");
synchronized (lock1) {
System.out.println("Thread T2 get lock1");
}
}
});
t1.start();
t2.start();
}
}
각 스레드의 동작은 1. mutual exclusive한 자원을 2. 하나 취득한 뒤에 다음 리소스를 추가로 기다리고 있으며, 3. 리소스 반환은 자신만 할 수 있고, 4. 2가지는 순환하면서 서로가 리소스를 반환하기로 기다리므로 데드락 상황을 만족한다.
역시나 각각 락을 하나씩 취득한 후에, 서로가 가지고 있는 락을 기다리면서 영원히 끝나지 않는다. (무한 버퍼링)
✔️ 데드락 해결 No Hold and Wait
T1이 lock1을 취득한 후에 반납하고, 이후에 lock2를 취득하도록 변경했다.
public class Main {
public static void main(String[] args) {
final Object lock1 = new Object();
final Object lock2 = new Object();
final Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread T1 get lock1");
}
synchronized (lock2) { // lock1을 반납한 후에 실행하도록 변경
System.out.println("Thread T1 get lock2");
}
});
final Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread T2 get lock2");
synchronized (lock1) {
System.out.println("Thread T2 get lock1");
}
}
});
t1.start();
t2.start();
}
}
이제는 더이상 데드락 상황을 만족시키지 않는다.
💋 참고자료
- https://www.baeldung.com/cs/os-deadlock
- https://www.youtube.com/watch?v=ESXCSNGFVto&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=7
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!