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를 추가해줘야 한다.)