📀 운영체제

14. 모니터와 자바 동기화: Chapter 6. Synchronization Tools (Part 4)

락꿈사 2022. 6. 13. 17:57

6.7 모니터

  • mutex락과 세마포는 타이밍 오류를 야기할 수 있음
  • 예시
    • 모든 프로세스는 mutex라는 이진 세마포 변수를 공유함
    • mutex 변수의 초기값은 1
    • 각 프로세스는 임계구역에 진입하기 전에 wait(mutex)를 실행해야 함
    • 각 프로세스는 임계구역을 나올 때 signal(mutex)를 실행해야 함 
  • 가정 1) 예시 프로세스의 순서가 지켜지지 않을 경우 (wait()과 signal() 연산의 순서가 뒤바뀌는 경우)
    • 두 프로세스가 동시에 임계구역에 있을 수 있음 (상호 배제 요구조건 위반)
    • 예시 코드
signal(mutex);
...
cirical section
...
wait(mutex);
  • 가정 2) signal(mutex)를 써야 할 곳에 잘못해서 wait(mutex)를 쓴 경우
    • 세마포를 사용할 수 없으므로 두 번째 wait() 호출에서 데드락에 걸림
    • 예시 코드
wait(mutex);
...
ciritical section
...
wait(mutex);
  • 가정 3) wait(mutex)나 signal(mutex) 또는 둘 다를 빠뜨렸을 경우
    • 상호 배제 요구 조건을 위반하거나 데드락에 걸림
  • 이러한 오류를 처리하기 위한 한가지 전략은 간단한 동기화 도구를 통합하여 고급 언어 구조물을 제공하는 것
  • 모니터는 이러한 고급 언어 구조물 중 하나임

 

 

모니터 사용법

  • 추상화된 데이터형ADT(Abstract data type)은 데이터와 이 데이터를 조작하는 함수들의 집합을 하나의 단위로 묶어 보호함

  • 모니터형
    • 상호 배제가 보장됨
    • 프로그래머가 정의한 일련의 연산자 집합을 포함하는 ADT
    • 변수 선언 포함
      • 변수들의 값은 그 형에 해당하는 인스턴스의 상태를 정의함
    • 변수들을 조작할 수 있는 프로시저 또는 함수들의 본채도 포함

  • 모니터ADT는 모니터 안에 항상 하나의 프로세스만이 활성화되도록 보장함

  • condition 변수를 갖는 모니터
    • 구현
      • condition 변수 x, y가 존재
      • 변수 x,y에 대한 준비 큐가 분리되어 operation들이 각각의 변수에 대해서 처리가 되고 동기화 됨
      • wait() 연산
        • condition형 변수에 호출될 수 있는 유일한 연산
        • x.wait(), y.wait()으로 사용
          • x.signal(), y.signal()이 호출될 때 까지 일시 중지 되어야 함을 의미
      •  signal() 연산
        • condition형 변수에 호출될 수 있는 유일한 연산
        • x.signal(), y.signal()으로 사용
          • 각각의 변수의 준비큐에 있는 하나의 일시 중시 프로세스를 재개함
          • 일시 중지 프로세스가 없을 경우 signal() 연산은 효과가 없음
    • Java의 모니터
      • 자바에서는 모니터 락을 사용함
        • 스레드 동기화을 위한 concurrency 매커니즘  
        • monitor lock, intrinsic lock이라고 부름
      • synchronized 키워드를 사용
        • 임계영역에 해당하는 코드 블록을 선언할 때 사용하는 자바 키워드
        • 해당 코드 블록(임계영역)에는 모니터락을 획득해야 진입 가능
        • 메소드에 선언하면 메소드 코드 블록 전체가 임계영역으로 지정됨
          • 이 때 모니터락을 가진 객체 인스턴스는 this 객체 인스턴스임
        • 아래 코드 예시 1), 예시 2) 참고
      • wait() / notify() 메소드 사용
        • java.lang.Object 클래스에 선언됨 (모든 자바 객체가 가진 메소드)
        • 어떤 스레드가 어떤 객체의 wait() 메소드를 호출
          • 해당 객체의 모니터 락을 획득하기 위해 대기 상태로 진입함
        • 어떤 스레드가 어떤 객체의 notify() 메소드를 호출
          • 해당 객체의 모니터에 대기중인 스레드 하나를 깨움 
        • 어떤 스레드가 어떤 객체의 notifyAll() 메소드를 호출
          • 해당 객체 모니터에 대기중인 스레드 전부를 깨우 
예시 1)
synchronized (object) {
  // critical section
 }
예시 2)
public synchronized void add() {
  // critical section
}
  • 예제 1
    • 모니터 락을 사용하지 않았으므로 동기화 문제 발생
// 방법 1) 동기화 문제 발생
// race condition 문제 존재

package com.os_study;

public class SynchExample1 {
    // 모니터
    static class Counter{
        public static int count = 0;
        public static void increment(){
            count++;
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run(){
            for (int i=0; i<10000; i++){
                Counter.increment();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        Thread threads[] = new Thread[5];
        for (int i=0; i<threads.length; i++){
            threads[i] = new Thread(new MyRunnable());
            threads[i].start();
        }
        for (int i=0; i< threads.length; i++)
            threads[i].join();
        System.out.println("counter =" + Counter.count);

    }
}

동기화 문제가 발생한 것 확인

  • 예제 2
    • 모니터 락을 사용하여 동기화 문제 해결
    • 메소드를 임계영역으로 만듦
// 방법 2) 동기화 문제 해결

package com.os_study;

public class SynchExample2 {
    // 모니터
    static class Counter{
        public static int count = 0;
        // acquire() 파트
        // synchronized를 사용하여 메서드를 임계영역으로 만들어 줌
        // 메소드가 길어질 경우 멀티쓰레드의 효율성이 떨어짐
        synchronized public static void increment(){
            count++;
        }
        // realease() 파트
    }

    static class MyRunnable implements Runnable{
        @Override
        public void run(){
            for (int i=0; i<10000; i++){
                Counter.increment();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        Thread threads[] = new Thread[5];
        for (int i=0; i<threads.length; i++){
            threads[i] = new Thread(new MyRunnable());
            threads[i].start();
        }
        for (int i=0; i< threads.length; i++)
            threads[i].join();
        System.out.println("counter =" + Counter.count);

    }
}

동기화 문제가 해결된 것 확인

  • 예제 3
    • 모니터 락을 사용하여 동기화 문제 해결
    • 메소드 안의 공유 변수에 접근하는 부분만 임계영역으로 지정
// 방법 3) 동기화 문제 해결

package com.os_study;

public class SynchExample3 {
    // 모니터
    static class Counter{
        // dummy 오브젝트를 생성하여 사용
        private static Object object = new Object();
        public static int count = 0;
        public static void increment(){
            synchronized (object){
                Counter.count++;
            }
        }

    }
    static class MyRunnable implements Runnable{
        Counter counter;

        public MyRunnable(Counter counter){
            this.counter = counter;
        }
        @Override
        public void run(){
            for (int i=0; i<10000; i++){
                counter.increment();

            }
        }
    }

    public static void main(String[] args) throws Exception{
        Thread threads[] = new Thread[5];
        for (int i=0; i<threads.length; i++){
            threads[i] = new Thread(new MyRunnable(new Counter()));
            threads[i].start();
        }
        for (int i=0; i< threads.length; i++)
            threads[i].join();
        System.out.println("counter =" + Counter.count);

    }
}

동기화 문제가 해결된 것 확인

  • 예제 4
    • 각 스레드가 서로 다른 Counter 객체(모니터 객체)를 참조함으로 인한 스레드 각각의 동기화가 이루어짐
    • static 변수 count에 대한 동기화가 이루어지지 않았으므로 동기화 문제 발생
// 방법 4) 동기화 문제 존재
// 쓰레드끼리의 동기화가 아니라 쓰레드 각각의 동기화가 이루어짐으로 인한 문제

package com.os_study;

public class SynchExample4 {
    // 모니터
    static class Counter{
        // 인스턴스의 객체 참조 함수를 사용하여 임계영역에 접근해주는 방법
        // this. 키워드를 사용하여 자기 객체 인스턴스의 모니터 락을 획득
        public static int count = 0;
        public void increment(){
            synchronized (this){
                Counter.count++;
            }
        }

    }
    static class MyRunnable implements Runnable{
        Counter counter;
        public MyRunnable(Counter counter){
            this.counter = counter;
        }
        @Override
        public void run(){
            for (int i=0; i<10000; i++){
                // 인스턴스의 메소드로 접근
                counter.increment();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        Thread threads[] = new Thread[5];
        for (int i=0; i<threads.length; i++){
            // Counter 객체 인스턴스, 즉 모니터 인스턴스가 5개가 생김
            // 모니터 안의 모든 메소드는 모니터 안 변수의 동기화를 의미함
            // 객체가 달라지면 모니터가 다 따로 생기는 것이므로 모니터끼리의 동기화는 이루어지지 않음
            // 동기화 문제 발생
            threads[i] = new Thread(new MyRunnable(new Counter()));
            threads[i].start();
        }
        for (int i=0; i< threads.length; i++)
            threads[i].join();
        System.out.println("counter =" + Counter.count);

    }
}

동기화 문제가 발생한 것 확인

  • 예제 5
    • 각 스레드가 하나의 Counter 객체(모니터 객체)를 참조함으로 인한 스레드 끼리의 동기화가 이루어짐
    • static 변수 count에 대한 동기화가 이루어져 동기화 문제 해결
// 방법 5) 동기화 문제 해결

package com.os_study;

public class SynchExample5 {
    // 모니터
    static class Counter{
        // 인스턴스의 객체 참조 함수를 사용하여 임계영역에 접근해주는 방법
        // this. 키워드를 사용하여 자기 객체 인스턴스의 모니터 락을 획득
        public static int count = 0;
        public void increment(){
            synchronized (this){
                Counter.count++;
            }
        }

    }
    static class MyRunnable implements Runnable{
        Counter counter = new Counter();
        public MyRunnable(Counter counter){
            this.counter = counter;
        }
        @Override
        public void run(){
            for (int i=0; i<10000; i++){
                counter.increment();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        Thread threads[] = new Thread[5];
        Counter counter = new Counter();
        for (int i=0; i<threads.length; i++){
            // Counter 객체 인스턴스, 즉 모니터 인스턴스가 5개가 생김
            // 모니터 인스턴스가 위에서 생성한 하나의 Counter 객체를 가지고 있음
            // 그 Counter 객체는 static count 변수에 접근함
            // 동기화 문제가 발생하지 않음
            threads[i] = new Thread(new MyRunnable(counter));
            threads[i].start();
        }
        for (int i=0; i< threads.length; i++)
            threads[i].join();
        System.out.println("counter =" + Counter.count);

    }
}

동기화 문제가 해결된 것 확인

 

 

6.6 라이브니스Livness

  • 위의 동기화 도구들을 사용함으로써 발생할 수 있는 결과는 교착상태deadlock이 될 수 있음
  • 데드락은 임계구역 문제 해결 조건 중 진행progress과 한정된 대기bounded waiting을 위반함
  • 라이브니스
    • 프로세스가 실행 수명주기 동안 진행되는 것을 보장하기 위해 시스템이 충족해야 하는 일련의 속성을 말함
    • deadlock은 라이브니스 실패의 예

 

 

교착 상태deadlock

  • 교착 상태
    • 두 개 이상의 프로세스들이 오로지 대기 중인 프로세스들 중 하나에 의해서만 야기될 수 있는 이벤트를 무한정 기다리는 상황

  • 예시
    • 위 그림에서 P0이 wait(S)를 실행하고 P1이 wait(Q)를 실행한다고 가정
      • P0이 wait(S)를 실행할 때 P0은 P1이 signal(S)를 실행할 때 까지 기다려야 함
      • P1이 wait(Q)를 실행할 때 P1은 P0이 signal(Q)를 실행할 때 까지 기다려야 함
    • 이들 signal() 연산들은 실행될 수 없기 때문에 P0과 P1은 교착 상태가 됨

 

 

우선순위 역전

  • 우선순위 역전Priority inversion문제
    • 높은 우선순위 프로세스가 현재 낮은 우선순위 프로세스 또는 연속된 낮은 우선순위 프로세스에 의해 접근되고 있는 커널 데이터를 읽거나 변경하려고 할 때 스케쥴링의 어려움이 발생
    • 통상 커널 데이터는 락에 의해 보호되기 때문에 낮은 우선순위 프로세스가 자원의 사용을 마칠 때까지 높은 우선순위 프로세스가 기다려야 함
  •  예시
    • 우선순위가 L < M < H  순서인 L, M, H 세 개의 프로세스가 있을 때
    • 프로세스 H가 세마포어 S가 필요
    • 자원 R은 현재 L에 의해 접근되고 있다고 가정
    • 프로세스 H는 L이 자원 사용을 마칠 때 까지 기다림
    • 이 순간 프로세스 M이 실행 가능 상태가 되어 자원 R을 선점함
    • 프로세스 M은 프로세스 H가 L이 자원을 양도할 때 까지 기다려야 하는 시간에 영향을 줌
  • 우선순위 상속 프로토콜priority-inheritance protocol
    • 더 높은 우선순위 프로세스가 필요로 하는 자원에 접근하는 모든 프로세스는 해당 자원의 사용이 끝날 때 까지 더 높은 우선순위를 상속받음
    • 따라서 위 예시에서 프로세스 M이 L의 실행을 선점하는 것을 방지함
    • 프로세스 L이 자원 R의 사용을 마치면 상속받은 우선순위를 방출하고 원래 우선순위로 되돌아감
    • 자원 R은 프로세스 H가 다음에 실행됨