on
운영체제 - 10~11
KOCW 운영체제 10~11강
양희재 교수님 강의를 듣고 쓴 글입니다. 10강과 11강을 같이 포스팅하는게 흐름이 안 끊길 것 같아서 한번에 포스팅 했습니다.
Process Synchronization
CPU의 Process Management 에서 가장 중요한 일은 CPU Scheduling
과 Process Synchronization
이다. 이전 강의에서는 CPU Scheduling에 대해서 공부했으니 이번에는 프로세스 동기화에 대해서 공부해 볼 것이다. 먼저 process의 관계를 잠깐 보고 가자.
- Independent process : 서로 영향을 안주는 process
- Cooperating process : 서로 영향을 미치는 process. ← common resource에 접근할 때 조심해야 한다.
Synchronization
: 동기화. Common resource를 공유하는 Cooperating process들 사이에서 순서를 잘 지켜서 data의 일관성을 유지 시키는 것.
동기화가 뭐가 중요하냐? 안되면 큰일이라도 나는가?
난다. 강의에서 사용한 예제를 다시 구현해봤다.
// 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-Section
은 BankAccount
class의 deposit(), withdraw()
method에서 발생하게 된다. 위에서 말했듯이 둘 중 하나의 thread에서 공유자원에 접근하는 중간에 다른 thread의 접근이 또 이뤄지면, 공유되는 data의 일관성을 잃을 수 있다는 것이다.
따라서 이 문제를 해결하기 위한 방법을 살펴보도록 하자.
- Common variable은
Mutual Exclusion
해야 한다. → 공유 자원에 오직 한번에 한 thread만 진입해야 한다. - Progress : 어떤 thread가 먼저 진입할 것인지 유한 시간 내에 정해야 한다.
- Bounded wating : 기다리는 시간은 유한하다.
Process/Thread Synchronization
이제 왜 동기화가 CPU Process Management의 중요한 기능인지 알 수 있었다. CPU Synchronization
은 Critical-Section 문제를 해결해주고, process의 실행 순서를 원하는대로 제어할 수 있게 해준다.
그리고 이렇게 동기화를 해주는 대표적인 도구에는 Semaphore, monitor 가 있다고 한다.
Semaphore
가장 전통적인 동기화 도구.
-
P 동작
acquire()
. Test 한다. -
V 동작
release()
. 증가시킨다.
저렇게만 보면 뭔소리인지 모를 것 같다. 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는 언제 누가 호출하는가?
다음 그림을 보도록 하자.
그림을 토대로 어떠한 상황을 시간 순서로 나열해보겠다.
상황 : 2개의 thread가 1개의 공통 자원에 접근하려는 상황. Semaphore
의 value=1
.
thread1
이ready Queue
에서 나와 CPU자원을 사용하기 시작.acquire()
호출. →value=0
이된다.acquire()
의if
조건을 만족 못시켰기에thread1
의Critical-Section
에 들어감.- 이 때 다른
thread2
가ready Queue
에서 나와 CPU 자원을 사용하기 시작.acquire()
호출. →value=-1
이 된다. if
조건 만족.thread2
는Semaphore Queue
에 갇힌다.- 이 때
thread1
의 동작이 끝남.release()
가 호출. →value=0
. release()
의if
의 조건을 만족하므로Semaphore Queue
에 갇혀있는thread2
해방.thread2
의Critical-Section
진입. 지정된 동작을 수행한 후 끝나면release()
호출. →value=1
이 된다.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를 추가해줘야 한다.)