프로세스 동기화는 여러 프로세스가 공유하는 자원의 일관성을 유지하는 것이다. 가령 여러 프로세스가 동시에 하나의 공유된 자원에 접근하려고 할 때 이 프로세스들의 순서를 정하여 데이터의 일관성을 유지시켜주어야 한다.
ex)
// Test.java
class Test {
public static void main(String[] args) throws InterruptedException {
BankAccount b = new BankAccount();
Parent p = new Parent(b);
Child c = new Child(b);
p.start(); // start(): 쓰레드를 실행하는 메서드
c.start();
p.join(); // join(): 쓰레드가 끝나기를 기다리는 메서드
c.join();
System.out.println("balance = " + b.getBalance());
}
}
// 계좌
class BankAccount {
int balance;
void deposit(int amount) {
balance = balance + amount;
}
void withdraw(int amount) {
balance = balance - amount;
}
int getBalance() {
return balance;
}
}
// 입금 프로세스
class Parent extends Thread {
BankAccount b;
Parent(BankAccount b) {
this.b = b;
}
public void run() { // run(): 쓰레드가 실제로 동작하는 부분(치환)
for (int i = 0; i < 100; i++)
b.deposit(1000);
}
}
// 출금 프로세스
class Child extends Thread {
BankAccount b;
Child(BankAccount b) {
this.b = b;
}
public void run() {
for (int i = 0; i < 100; i++)
b.withdraw(1000);
}
}
멀티 스레딩에 문제점에서 나오듯, 동일한 자원을 동시에 접근하는 작업(e.g. 공유하는 변수 사용, 동일 파일을 사용하는 등)을 실행하는 코드 영역을 Critical Section 이라 칭한다.
위 예제에서 공통 변수는 계좌의 잔액이다. 이에 접근하는 프로세스의 코드를 보면 다음과 같다. 이러한 공통변수 구역을 임계구역이라고 한다.
void deposit(int amount) {
balance = balance + amount; //출금
}
void withdraw(int amount) {
balance = balance - amount; //입금
}
하드웨어 기반 해결책으로써, 동시에 공유 자원에 접근하는 것을 막기 위해 Critical Section 에 진입하는 프로세스는 Lock 을 획득하고 Critical Section 을 빠져나올 때, Lock 을 방출함으로써 동시에 접근이 되지 않도록 한다.
한계
다중처리기 환경에서는 시간적인 효율성 측면에서 적용할 수 없다.
세마포는 동기화를 위해 만들어진 소프트웨어로서, 대표적인 동기화 도구이다.
소프트웨어상에서 Critical Section 문제를 해결하기 위한 동기화 도구
class Semaphore {
int value; // number of permits
Semaphore(int value) {
// ...
}
void acquire() {
value--;
if (value < 0) {
// add this process/thread to list
// block
}
}
void release() {
value++;
if (value <= 0) {
// remove a process P from list
// wakeup P
}
}
}
위 코드에서 acquire() 는 value값을 감소시키고 만약 value값이 0보다 작으면 이미 해당 임계구역에 어느 프로세스가 존재한다는 의미이므로 현재 프로세스는 접근하지 못하도록 막아야한다. 이를 list라는 기다리는 줄에 추가한 뒤 block을 걸어준다.(list는 일반적으로 큐로 되어있다.)
release() 는 value값을 증가시키고, 만약 value값이 0보다 같거나 작으면 임계구역에 진입하려고 대기하는 프로세스가 list에 남아있다는 의미이므로 그 중에서 하나를 꺼내어 임계구역을 수행할 수 있도록 해주어야 한다.
세마포를 그림으로 나타내면 위와 같다. list는 실제로 큐로 볼 수 있다. acquire()에 의해 block되는 프로세스는 세마포 내부에 있는 큐에 삽입된 후, 다른 프로세스가 임계구역을 나오면서 release()를 호출하여 세마포 큐에 있는 프로세스를 깨워야 한다.(다시 ready queue로 보낸다.)
위에서 살펴본 것처럼 세마포는 일반적으로 Mutual exclusion을 위해 사용된다.
import java.util.concurrent.Semaphore; // 세마포를 사용하기 위해 파일 가장 위에 추가해야 한다.
class BankAccount {
int balance;
Semaphore sem;
BankAccount() { // BankAccount 클래스의 생성자가 호출되면 세마포를 만든다.
sem = new Semaphore(1); // value 값을 1로 초기화한다.
}
void deposit(int amount) {
try {
sem.acquire(); // 임계구역에 들어가기를 요청한다.
} catch (InterruptedException e) {}
/* 임계 구역 */
int temp = balance + amount;
System.out.print("+");
balance = temp;
sem.release(); // 임계구역에서 나간다.
}
void withdraw(int amount) {
try {
sem.acquire();
} catch (InterruptedException e) {}
/* 임계 구역 */
int temp = balance - amount;
System.out.print("-");
balance = temp;
sem.release();
}
int getBalance() {
return balance;
}
}
예를 들어, 프로세스가 P1, P2 두 개가 있다고 가정하자. 원하는 순서는 P1, P2 순으로 실행하기를 원한다. 그러면 아래와 같이 설정해줄 수 있다.
먼저, 세마포로 감싼 구역에 들어갈 수 있는 프로세스 개수를 정하는 value값을 0으로 설정한다.
- sem value = 0;
ex) java
class BankAccount {
int balance;
Semaphore sem, semOrder;
BankAccount() {
sem = new Semaphore(1);
semOrder = new Semaphore(0); // Ordeing을 위한 세마포
}
void deposit(int amount) {
try {
sem.acquire();
} catch (InterruptedException e) {}
int temp = balance + amount;
System.out.print("+");
balance = temp;
sem.release();
semOrder.release(); // block된 출금 프로세스가 있다면 깨워준다.
}
void withdraw(int amount) {
try {
semOrder.acquire(); // 출금을 먼저하려고 하면 block한다.
sem.acquire();
} catch (InterruptedException e) {}
int temp = balance - amount;
System.out.print("-");
balance = temp;
sem.release();
}
int getBalance() {
return balance;
}
}
종류
- 카운팅 세마포
가용한 개수를 가진 자원 에 대한 접근 제어용으로 사용되며, 세마포는 그 가용한 자원의 개수 로 초기화 된다. 자원을 사용하면 세마포가 감소, 방출하면 세마포가 증가 한다.
- 이진 세마포
MUTEX 라고도 부르며, 상호배제의 (Mutual Exclusion)의 머릿글자를 따서 만들어졌다. 이름 그대로 0 과 1 사이의 값만 가능하며, 다중 프로세스들 사이의 Critical Section 문제를 해결하기 위해 사용한다.
단점
- Busy Waiting(바쁜 대기)
Spin lock이라고 불리는 Semaphore 초기 버전에서 Critical Section 에 진입해야하는 프로세스는 진입 코드를 계속 반복 실행해야 하며, CPU 시간을 낭비했었다. 이를 Busy Waiting이라고 부르며 특수한 상황이 아니면 비효율적이다. 일반적으로는 Semaphore에서 Critical Section에 진입을 시도했지만 실패한 프로세스에 대해 Block시킨 뒤, Critical Section에 자리가 날 때 다시 깨우는 방식을 사용한다. 이 경우 Busy waiting으로 인한 시간낭비 문제가 해결된다.
- Deadlock(교착상태)
세마포가 Ready Queue 를 가지고 있고, 둘 이상의 프로세스가 Critical Section 진입을 무한정 기다리고 있고, Critical Section 에서 실행되는 프로세스는 진입 대기 중인 프로세스가 실행되야만 빠져나올 수 있는 상황을 지칭한다.
고급 언어의 설계 구조물로서, 개발자의 코드를 상호배제 하게끔 만든 추상화된 데이터 형태이다.
공유자원에 접근하기 위한 키 획득과 자원 사용 후 해제를 모두 처리한다. (세마포어는 직접 키 해제와 공유자원 접근 처리가 필요하다. )