동기화 코드로 확인하기

Chan Young Jeong·2023년 3월 4일
0

운영체제

목록 보기
9/11
post-thumbnail

Bank Account Problem (은행 계좌 문제)

은행에서는 하나의 계좌에 입금과 출금을 할 수 있습니다. 여기서 계좌는 공유 자원에 해당하고, 입금과 출금 각각은 공유 자원에 접근하고자 하는 프로세스라고 볼 수 있습니다. 이를 자바로 구현한 코드는 다음과 같습니다.

public class ThreadSyncEx {
    public static void main(String[] args) {
        Runnable thread = new CreateThread();
        // 2개의 작업 스레드 생성
        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);

        thread1.setName("스레드1");
        thread2.setName("스레드2");

        thread1.start();
        thread2.start();
    }
}

class Money {
    // 현재 가지고 있는 금액
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    public void withdraw(int money) {
        // 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
        if (myMoney >= money) {
            // 스레드 동시 접근을 위한 코드
            try {
                Thread.sleep(1000);
            } catch (Exception e) {}

            // 출금
            myMoney -= money;
            System.out.printf("스레드: %s , 출금액: %d원  남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
            return;
        }
        System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);

    }
}

class CreateThread implements Runnable {
    Money myMoney = new Money();

    public void run() {
        while (myMoney.getMyMoney() > 0) {
            // 1000 ~ 5000원씩 출금
            int money = (int)(Math.random() * 5 + 1) * 1000;

            // 출금 실행 코드
            myMoney.withdraw(money);

        }
    }
}

실행 결과는 다음과 같습니다. 뭔가 이상하죠? 남은 금액도 이상하고 코드상으로는 남은금액이 마이너스가 될 수 없는데 말입니다.
그 이유는 당연히 코드 동기화가 되어 있지 않아서 입니다.

myMoney -= money

코드상에서 가장 문제가 있는 부분은 이 공유 자원의 값을 변경하는 부분입니다. 자바 코드상으로는 이 한 줄만 실행되는 것 같지만 로우 레벨로 내려가서 바이트코드를 보면 여러 줄로 구현이 되어 있기 때문입니다. 이를 해결하기 위해서는 공유 자원에 접근하는 쓰레드는 하나만 존재하도록 관리해야합니다.

세마포 이용하기

은행계좌 문제에 세마포를 적용해보자. 위에서 임계구역은 Money 클래스 내부에서 myMoney의 값을 변경하는 부분이다. 따라서 임계 구역 앞뒤로 acquire와 release를 적용하면 다음과 같이 바꿀 수 있다.

  • Semaphore(int permits, boolean fair) : 공유 자원에 접근할 수 있는 스레드의 수를 permits 매개변수를 통해 지정할 수 있다. fair가 false면 순서가 보장되지 않는다.

  • acquire() throws InterruptedException
    : semaphore 의 permit 을 하나 가져간다. ( -1 시킨다 )
    permit 이 양수였을 경우에는 스레드가 계속 동작을 계속하고 그렇지 않으면 permit 을 획득할 때까지 혹은 interrupt 되기 전까지 waiting 한다.

  • release()
    : permit을 release한다 ( +1 시킨다)
    permit을 획득하려고 대기하는 스레드가 있으면 fair 정책에 따라 해당 스레드가 깨어난다.

class Money {
	
    private int myMoney = 10000;
    Semaphore semaphore;

    public MoneyWithSemaphore() {
        semaphore = new Semaphore(1); // value 값을 1로 초기화
    }
    public int getMyMoney() {
        return myMoney;
    }

    public void withdraw(int money) {

        try{
            semaphore.acquire(); //임계 구역에 들어가기를 요청
        }catch (InterruptedException e){}

        try{
            // 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
            if (myMoney >= money) {
                // 스레드 동시 접근을 위한 코드
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {}

                // 출금
                myMoney -= money;
                System.out.printf("스레드: %s , 출금액: %d원  남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
                return;
            }
            System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
        }finally {
            semaphore.release();
        }

    }
}

정상적으로 동작하는 걸 확인할 수 있다.

세마포 이용해서 실행 순서 제어하기

세마포는 상포 배제(Mutual Exclusion)뿐만 아니라 프로세스간 순서를 제어하기 위해서도 사용됩니다. 예를 들어 프로세스 P1, P2 두 개가 있다고 가정하면, 원하는 순서는 P1, P2 순으로 실행하기를 원합니다. 그러면 아래와 같이 설정해주면 됩니다.

  • sem value = 0 ;
P1P2
sem.acquire()
Section1Section2
sem.release()

P1이 먼저 실행되는 경우

  • P1이 먼저 실행되는 경우에는 먼저 Section1에 들어간 후 실행이 끝나면 sem.release()가 호출되면서 value 값이 1 증가됩니다.

  • 그리고 나서 P2가 sem.acquire()를 실행하면서 Section2에 진입할 수 있게 됩니다.

P2가 먼저 실행되는 경우

중요한 부분은 이 부분입니다.

  • P2가 먼저 실행이 되면 sem.acquire()을 호출하게 됩니다.
  • 하지만 처음에 value의 값을 0으로 초기화 해주었기 때문에 value 값이 1 감소해서 -1이 됩니다. value 값이 마이너스가 되면 해당 스레드는 Block이 됩니다.
  • 이후 P1의 Section1이 실행되고 sem.release()를 호출하게 됩니다.
  • 그렇게 되면 Block된 프로세스 중 P2가 깨어나고 Section2를 실행할 수 있게됩니다.

따라서 다음과 같이 세마포를 작성하면 P1 - > P2 순서대로 실행시킬 수 있습니다.

코드로 확인하기

이번에는 출금 스레드와 입금 스레드를 나누어서 생성하였습니다. 그리고 semaphoreOrder라는 순서 제어를 위한 세마포를 추가해주었습니다. 주의할 점은 semaphoreOrder는 처음에 value 값을 0으로 초기화 주었고, 입금이 항상 먼저 실행되게끔 하기 위해 withdraw메서드 안에 다음과 같이 코드를 작성해 주었습니다.

try{
            semaphoreOrder.acquire(); // 순서제어를 위한 세마포
            semaphore.acquire(); // 상호 배제를 위한 세마포
        }catch (InterruptedException e){}

전체 코드는 다음과 같습니다.

import java.util.concurrent.Semaphore;

public class ThreadSyncEx3 {
    public static void main(String[] args) {

        MoneyWithSemaphoreWithOrder money = new MoneyWithSemaphoreWithOrder();

        // 2개의 작업 스레드 생성
        Thread withdrawThread = new Thread(new WithdrawThread(money));
        Thread depositThread = new Thread(new DepositThread(money));

        withdrawThread.setName("출금 스레드");
        depositThread.setName("입금 스레드");

        withdrawThread.start();
        depositThread.start();
    }
}

class MoneyWithSemaphoreWithOrder {
    // 현재 가지고 있는 금액
    private int myMoney = 10000;
    Semaphore semaphore,semaphoreOrder;

    public MoneyWithSemaphoreWithOrder() {
        semaphore = new Semaphore(1); // value 값을 1로 초기화
        semaphoreOrder = new Semaphore(0); // value 값을 0으로 초기화
    }

    public int getMyMoney() {
        return myMoney;
    }

    public boolean withdraw(int money) {

        try{
            semaphoreOrder.acquire();
            semaphore.acquire(); //임계 구역에 들어가기를 요청
        }catch (InterruptedException e){}

        try{
            return doWithdraw(money); // 임계 구역
        }finally {
            semaphore.release(); //임계 구역에서 나가면서 lock을 release한다.
        }
    }

    private boolean doWithdraw(int money) {
        if (myMoney >= money) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {}
            myMoney -= money;
            System.out.printf("스레드: %s , 출금액: %d원  남은금액: %d원 \n",Thread.currentThread().getName(), money, myMoney);
            return true;
        }
        System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
        return false;
    }


    public void deposit(int money) {
        try{
            semaphore.acquire(); //임계 구역에 들어가기를 요청
        }catch (InterruptedException e){}

        try{
            doDeposit(money); // 임계 구역
        }finally {
            semaphore.release(); //임계 구역에서 나가면서 lock을 release한다.
            semaphoreOrder.release();
        }
    }
    private void doDeposit(int money) {
        try {
            Thread.sleep(10);
        } catch (Exception e) {}
        myMoney += money;
        System.out.printf("스레드: %s , 입금액: %d원  남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
    }


}

class DepositThread implements Runnable {
    private MoneyWithSemaphoreWithOrder myMoney;

    DepositThread(MoneyWithSemaphoreWithOrder myMoney) {
        this.myMoney = myMoney;
    }

    @Override
    public void run() {
        while (true){
            int money = (int)(Math.random() * 5 + 1) * 1000;
            myMoney.deposit(money);
        }

    }
}

class WithdrawThread implements Runnable {
    private MoneyWithSemaphoreWithOrder myMoney;

    WithdrawThread(MoneyWithSemaphoreWithOrder myMoney) {
        this.myMoney = myMoney;
    }

    @Override
    public void run() {
        while (true) {
            // 1000 ~ 5000원씩 출금
            int money = (int)(Math.random() * 10 + 1) * 1000;
            // 출금 실행 코드. 실패시 true 반환
            boolean denied = !myMoney.withdraw(money);

        }
    }
}

모니터 이용하기

자바에서는 synchronized 키워드를 이용해 모니터를 이용한 동기화를 지원하고 있습니다.
synchronized 를 사용하는 방법으로는 메서드 앞에 키워드 명시, 인스턴스로 사용하기가 있다. 동기화가 필요한 메서드 앞에 synchronized 키워드만 붙여주면 편리하게 동기화를 적용할 수 있다.

  • 인스턴스로 사용하려면 메서드 내부에서 synchronized (메서드) { 구현 } 으로 사용할 수 있다.
class MoneyWithMonitor {
    // 현재 가지고 있는 금액
    private int myMoney = 10000;
    public int getMyMoney() {
        return myMoney;
    }

    public synchronized boolean withdraw(int money) {

        try{
            return doWithdraw(money); // 임계 구역
        }finally {

        }
    }

    private boolean doWithdraw(int money) {
        if (myMoney >= money){
            try {
                Thread.sleep(10);
            } catch (Exception e) {}
            myMoney -= money;
            System.out.printf("스레드: %s , 출금액: %d원  남은금액: %d원 \n",Thread.currentThread().getName(), money, myMoney);
            return true;
        }
        System.out.printf("스레드: %s , 출금액: %d원 출금 거부 \n",Thread.currentThread().getName(),money);
        return false;
    }


    public synchronized void deposit(int money) {

        try{
            doDeposit(money); // 임계 구역
        }finally {
        }
    }
    private void doDeposit(int money) {
        try {
            Thread.sleep(10);
        } catch (Exception e) {}
        myMoney += money;
        System.out.printf("스레드: %s , 입금액: %d원  남은금액: %d원 \n", Thread.currentThread().getName(), money, myMoney);
    }


}

실행결과를 확인해보겠습니다. 동기화가 잘 이루어진 걸 확인할 수 있습니다.

0개의 댓글