'자바' 코드를 기반으로 설명됩니다.
A 프로세스가
동작하기 위해서, a1
리소스를 점유중인 상태에서 b1
라는 자원이 필요하고, B 프로세스
는 동작하기 위해서 b1
리소스를 점유중인 상태에서 a1
이라는 자원이 필요하다고 하면 어떨까?작업A
는 자신의 작업을 마치기 위해, 자원 b1
이 필요하고, 작업B
는 마찬가지로 a1
이 필요한 상태입니다.상호 배제
- 교착 상태가 일어나는 근본적인 원인은, 하나의 프로세스만 해당 자원을 이용 가능했기 때문입니다.
- 동기화 전략 등에서 소개되는 다양한 lock 기법 등을 활용하여 동기화 제어를 한다면, 하나의 프로세스만 접근 가능하도록 통제하기 때문에 이러한 문제가 발생할 수 있습니다.
점유와 대기
- 한 프로세스가 어떤 자원을 할당 받은 상태에서 다른 자원을 할당받기를 기다린다면 교착 상태가 발생할 수 있습니다.
- 위의
작업A, 작업B
예제가 딱 그 상태 입니다.작업A
는 작업을 완료하기 위해,a1
및b1
리소스를 할당 받기 위해 대기하는 상황이 발생합니다.
비선점
- 비선점이란, 자원을 이용하는 프로세스의 작업이 끝나야만 비로소 자원을 이용할 수 있다는 것을 의미합니다.
- 위 예제
작업A
가 원하는b1
리소스는,작업B
가b1
리소스를 반납해줘야만 가능하지만, 그렇지 않게 설계되어 있습니다.- 즉, 어떤 프로세스도 다른 프로세스의 자원을 강제로 빼앗지 못하는 경우 (비선점), 교착 상태가 발생할 수 있습니다.
원형 대기
- 프로세스와 프로세스가 요청한 자원이 원의 형태를 이루는 경우입니다.
- 각각의 프로세스가 서로 점유한 자원을 할당받기 위해 원의 형태로 대기할 경우, 교착 상태가 발생할 수 있습니다.
- 일상적인 예시로, 중고거래를 위해 만난 사람끼리 "먼저 물건을 주세요", "먼저 돈을 입금해주세요" 하고, 무한히 반복하며 거래가 되지 않는 상태라고 말할 수 있습니다.
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void threadA() {
synchronized (lockA) {
System.out.println("ThreadA: lockA 점유");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("ThreadA: lockB 점유");
}
}
}
public void threadB() {
synchronized (lockB) {
System.out.println("ThreadB: lockB 점유");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("ThreadB: lockA 점유");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
new Thread(example::threadA).start();
new Thread(example::threadB).start();
}
}
threadA
와 threadB
는 각각 하나의 자원을 가지고, 0.1초 뒤, 반대측에서 점유중인 lock에 대해 접근을 하는 코드 입니다.ThreadA: lockA 점유
ThreadB: lockB 점유
... 다음 응답이 없음 ...
교착 상태 예방
은 위의 소개된 4가지 필요 조건 중 하나라도, 깨트려 deadlock 이 절때 발생하지 않도록 만드는 방법입니다.상호 배제 제거
- 예를 들면, 일반 Lock 을
세마포
등으로 변경해, 여러 쓰레드에서 동시에 접근이 가능하도록 만들 수도 있습니다.Java
에서는, 읽기 작업에 대해서 동시에 여러 스레드가 접근 가능하도록 하는ReentrantReadWriteLock
이라는걸 제공해줍니다.- 다음과 같이 코드를 변경한다면, deadlock은 해결됩니다.
public class NoDeadlockReadExample {
private final ReentrantReadWriteLock lockA = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock lockB = new ReentrantReadWriteLock();
public void threadA() {
lockA.readLock().lock();
try {
System.out.println("ThreadA: lockA 읽기 접근");
try { Thread.sleep(100); } catch (InterruptedException e) {}
lockB.readLock().lock();
try {
System.out.println("ThreadA: lockB 읽기 접근");
} finally {
lockB.readLock().unlock();
}
} finally {
lockA.readLock().unlock();
}
}
public void threadB() {
lockB.readLock().lock();
try {
System.out.println("ThreadB: lockB 읽기 접근");
try { Thread.sleep(100); } catch (InterruptedException e) {}
lockA.readLock().lock();
try {
System.out.println("ThreadB: lockA 읽기 접근");
} finally {
lockA.readLock().unlock();
}
} finally {
lockB.readLock().unlock();
}
}
public static void main(String[] args) {
NoDeadlockReadExample example = new NoDeadlockReadExample();
new Thread(example::threadA).start();
new Thread(example::threadB).start();
}
}
ThreadA: lockA 읽기 접근
ThreadB: lockB 읽기 접근
ThreadB: lockA 읽기 접근
ThreadA: lockB 읽기 접근
Process finished with exit code 0
점유와 대기 깨기
- 점유와 대기 조건을 깨트려, deadlock 을 회피할 수도 있습니다.
threadA
와threadB
가 필요한 자원 2가지를 모두 먼저 점유해버리는 방법으로 처리하여 회피할 수도 있습니다.
public class NoHoldAndWaitExample {
private final Lock lockA = new ReentrantLock();
private final Lock lockB = new ReentrantLock();
public void threadA() {
while (true) {
boolean gotLockA = lockA.tryLock();
boolean gotLockB = lockB.tryLock();
if (gotLockA && gotLockB) {
try {
System.out.println("ThreadA: lockA, lockB 점유");
break; // 작업 후 종료
} finally {
lockB.unlock();
lockA.unlock();
}
}
// 하나라도 못 잡으면 전부 반환하고 재시도
if (gotLockA) lockA.unlock();
if (gotLockB) lockB.unlock();
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
public void threadB() {
while (true) {
boolean gotLockA = lockA.tryLock();
boolean gotLockB = lockB.tryLock();
if (gotLockA && gotLockB) {
try {
System.out.println("ThreadB: lockA, lockB 점유");
break; // 작업 후 종료
} finally {
lockB.unlock();
lockA.unlock();
}
}
// 하나라도 못 잡으면 전부 반환하고 재시도
if (gotLockA) lockA.unlock();
if (gotLockB) lockB.unlock();
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
NoHoldAndWaitExample example = new NoHoldAndWaitExample();
new Thread(example::threadA).start();
new Thread(example::threadB).start();
}
}
ThreadA: lockA, lockB 점유
ThreadB: lockA, lockB 점유
Process finished with exit code 0
은행가 알고리즘
이 대표적입니다.은행가 알고리즘 살짝
- 자원을 할당할 때 마다,
모든 프로세스가 결국 자원을 얻고 정상 종료를 할 수 있는 안전 상태 인지
시뮬레이션을 통해 확인 후, 자원을 할당합니다.- 만약 알고리즘에 의해, 위험한 상황이 예상된다면 자원 할등을 거부해서 교착 상태 발생을 회피하는 방식입니다.
자원 선점을 통해 회복
하거나, 문제 프로세스를 강제 종료
하여 해결하는 방법들이 존재합니다.자원 선점을 통한 회복
- 교착 상태가 해결될 때까지 다른 프로세스로부터 강제로 자원을 빼앗아 한 프로세스에 몰아주는 것을 의미