운영체제 - 10~11

KOCW 운영체제 10~11강

양희재 교수님 강의를 듣고 쓴 글입니다. 10강과 11강을 같이 포스팅하는게 흐름이 안 끊길 것 같아서 한번에 포스팅 했습니다.


Process Synchronization

CPU의 Process Management 에서 가장 중요한 일은 CPU SchedulingProcess Synchronization이다. 이전 강의에서는 CPU Scheduling에 대해서 공부했으니 이번에는 프로세스 동기화에 대해서 공부해 볼 것이다. 먼저 process의 관계를 잠깐 보고 가자.



동기화가 뭐가 중요하냐? 안되면 큰일이라도 나는가?

난다. 강의에서 사용한 예제를 다시 구현해봤다.

// BankAccount Class : process 가 공유하는 common resource class
public class BankAccount {
    int balance;

    public BankAccount(int balance) {
        this.balance = balance;
    }

    public void deposit(int money) { 
        this.balance += money;
    }

    public void withdraw(int money) { 
        this.balance -= money;
    }

    public int getBalance() {
        return balance;
    }
}

강의에서는 은행계좌를 예시로 들었다. 간단하게 말하자면, A라는 사람이 ‘출금’을 할 때 B라는 사람이 동시에 ‘입금’을 할 때 동기화 문제가 생긴다는 것이다. 여기서 문제가 생기는 이유는 간단하다. 같은 계좌의 돈에 2명이 동시에 접근하기 때문이다.

예시 코드에서는 입금의 행위인 deposit()이라는 method를 만들었고, 출금이라는 행위인 withdraw()라는 method를 만들었다. 다음으로는 이 Common resource를 사용할 thread class를 만들 것이다.


입금하는 자(DepositPerson Class)

class DepositPerson extends Thread {
    BankAccount account;

    public DepositPerson(BankAccount account) {
        this.account = account;
    }

    public void run() {
        int i = 100000;
        while (i-->0) {
            account.deposit(1000);
        }
    }
}

Thread class를 상속받았다. 하나의 공통된 자원인 BankAccount를 사용할 것이기에 field에 선언해주고 Contructor로 초기화해줬다. 동작은 단순하게 1000원 만큼의 돈을 i번 계좌에 입금하는 것이다. 다음으로 출금하는 자 코드를 보자.


출금하는 자(WithdrawPerson Class)

class WithdrawPerson extends Thread {
    BankAccount account;

    public WithdrawPerson(BankAccount account) {
        this.account = account;
    }

    public void run() {
        int i = 100000;
        while (i-->0) {
            account.withdraw(1000);
        }
    }
}

동작은 단순하게 1000원 만큼의 돈을 i번 계좌에 출금하는 것이다.


Main test code (출금하는 자 vs 입금하는 자)

각각의 출금/입금하는 자 class는 똑같은 횟수의 입금 또는 출금을 하도록 설정했다. 그리고 통장의 돈은 -부호를 가질 수 있다고 가정해보자.

똑같은 횟수의 입금과 출금이 이뤄지면 상식적으로 결과는 입금/출금 하기 전의 돈이 남을 것이다. 테스트 코드를 작성하고 결과를 보도록 하겠다.

// Test Code
class BankTest {
    public static void main(String[] args) throws InterruptedException {
        BankAccount commonAccount = new BankAccount(3000); // 공통 자원인 A계좌 선언. 처음에 A계좌에는 3000원이 들어있다고 하자. 
        DepositPerson depositPerson = new DepositPerson(commonAccount); // A계좌를 갖고 입금만 하는 자.
        WithdrawPerson withdrawPerson = new WithdrawPerson(commonAccount); // A계좌를 갖고 출금만 하는 자.
        depositPerson.start(); // 입금만 하는 자 시작.
        withdrawPerson.start(); // 출금만 하는 자 시작.
        depositPerson.join(); // 해당 thread 종료될 때까지 wait
        withdrawPerson.join(); // 해당 thread 종료될 때까지 wait
        System.out.println("get balance = " + commonAccount.getBalance()); // 남은 돈 확인. 정상적인 상황에서는 3000원이 그대로 있어야 함.
    }
}

설명은 주석을 참고하면 될 것 같다. 바로 결과를 보도록 하자.


get balance = -102000 // 첫 시도
get balance = -397000 // 2번째
get balance = -241000 // 3번째

코드를 돌리면 돌릴수록 매번 다른 값이 나온다.


3000원이 아니라 왜 다른 값이 나오는가?

공유하는 자원에 대해서 동시에 접근하다보니 저러한 문제가 나왔다.

public deposit(I)V
  L0
   LINENUMBER 10 L0
   ALOAD 0
   DUP
   GETFIELD BankAccount.balance : I
   ILOAD 1
   IADD
   PUTFIELD BankAccount.balance : I
  L1
   LINENUMBER 11 L1
   RETURN
  L2
   LOCALVARIABLE this LBankAccount; L0 L2 0
   LOCALVARIABLE money I L0 L2 1
   MAXSTACK = 3
   MAXLOCALS = 2

따라서 BankAccount Class의 deposit(), withdraw() method의 bytecode를 한번 보도록 하자. 우리에겐 balance += money라는 코드 한 줄이겠지만, JVM에게는 여러줄의 bytecode의 모임으로 한 줄을 표현해야 할 것이다.

정확히… 저 코드를 분석할 수는 없지만, 중요한 것은 Atomic해야 한다는 것. 그렇지 못한다면 코드 한줄을 이루는 저 여러줄의 bytecode사이로 다른 thread의 코드가 실행될 수 있다는 것이다. 따라서 Critical-Section 문제가 나타나는 것이다.


Critical-Section Problem

Common variable 을 update하는 구간에서 발생하는 문제.

해당 예시의 Critical-SectionBankAccount class의 deposit(), withdraw() method에서 발생하게 된다. 위에서 말했듯이 둘 중 하나의 thread에서 공유자원에 접근하는 중간에 다른 thread의 접근이 또 이뤄지면, 공유되는 data의 일관성을 잃을 수 있다는 것이다.

따라서 이 문제를 해결하기 위한 방법을 살펴보도록 하자.

  1. Common variable은 Mutual Exclusion해야 한다. → 공유 자원에 오직 한번에 한 thread만 진입해야 한다.
  2. Progress : 어떤 thread가 먼저 진입할 것인지 유한 시간 내에 정해야 한다.
  3. Bounded wating : 기다리는 시간은 유한하다.


Process/Thread Synchronization

이제 왜 동기화가 CPU Process Management의 중요한 기능인지 알 수 있었다. CPU Synchronization은 Critical-Section 문제를 해결해주고, process의 실행 순서를 원하는대로 제어할 수 있게 해준다.

그리고 이렇게 동기화를 해주는 대표적인 도구에는 Semaphore, monitor 가 있다고 한다.


Semaphore

가장 전통적인 동기화 도구.


저렇게만 보면 뭔소리인지 모를 것 같다. Pseudo code로 각각의 기능을 자세히 살펴보자.

int value; // 한번에 접근 가능한 process/thread 개수. number of permit

void acquire() {
    value--; // 현재 process의 동작이 시작하려고 하기에 접근 가능한 개수 -1
    if (value < 0) { // value<0이면 더 이상의 접근은 불가능 하다.
        add this process in 'Process Queue' // process를 대기열에 넣는다.
        block; // Stop this process // 지금 process wait
    }
}

void release() {
    value++; // 현재 process의 동작이 끝나서 접근 가능한 개수 +1
    if (value <= 0) { // value<=0이면 대기열에 process가 존재하는 것이기에 실행시켜줘야 한다.
        remove a process from 'Process Queue' // 대기열에서 process를 제거.
        wake up that Process // 제거한 process는 Queue에서 해방된다.
    }
}

설명은 주석을 보면 된다. 그렇다면 저 Queue는 도대체 어디에 있으며 acquire()같은 method는 언제 누가 호출하는가?


다음 그림을 보도록 하자.

os11_1

그림을 토대로 어떠한 상황을 시간 순서로 나열해보겠다.

상황 : 2개의 thread가 1개의 공통 자원에 접근하려는 상황. Semaphorevalue=1.

  1. thread1ready Queue에서 나와 CPU자원을 사용하기 시작. acquire()호출. → value=0이된다.
  2. acquire()if조건을 만족 못시켰기에 thread1Critical-Section에 들어감.
  3. 이 때 다른 thread2ready Queue에서 나와 CPU 자원을 사용하기 시작. acquire()호출. → value=-1이 된다.
  4. if조건 만족. thread2Semaphore Queue에 갇힌다.
  5. 이 때 thread1의 동작이 끝남. release()가 호출. → value=0.
  6. release()if의 조건을 만족하므로 Semaphore Queue에 갇혀있는 thread2 해방.
  7. thread2Critical-Section 진입. 지정된 동작을 수행한 후 끝나면 release()호출. → value=1이 된다.
  8. release()if조건문 만족 못했기에 그냥 통과.

→ Mutual Exclusion 을 보장


동기화 문제를 해결한 결과 코드

이제 위에서 배운 내용으로 아까의 은행계좌 문제를 해결해보자.

// 수정된 BankAccount Class.
public class BankAccount {
    int balance;
    Semaphore sem; // Semaphore 생성

    public BankAccount(int balance) {
        this.balance = balance;
        this.sem = new Semaphore(1); // 동시 접근 가능한 개수 == 1
    }

    public void deposit(int money) throws InterruptedException { // depositPerson 의 Critical-Section
        sem.acquire();
        this.balance += money;
        sem.release();
    }

    public void withdraw(int money) throws InterruptedException { // withdrawPerson 의 Critical-Section
        sem.acquire();
        this.balance -= money;
        sem.release();
    }

    public int getBalance() {
        return balance;
    }
}

이렇게 해주면 결과는 3000으로 상식적으로 잘 나오게 된다. (물론 입금/출금하는 자 class 에서 Exception code를 추가해줘야 한다.)