⚠️ 해당 포스팅은 인프런 공룡책 강의를 듣고 개인적으로 정리하는 글입니다. 정확하지 않은 정보가 있을 수 있으니 주의를 요합니다.
Chapter 7 CPU Synchronization Example
mutex
가 있다.mutex
는 버퍼 풀에 접근할 때 상호 배제를 제공하는 역할을 맡게된다.empty
, full
이 있다.empty
는 초기값을 n으로, full
은 초기값을 0으로 배정한다.while(true) {
/* produce an item in next_produced */
. . .
wait(empty); // buffer
wait(mutex); // mutex
/* add next_produced to the buffer */
signal(mutex);
signal(full);
}
while(true) {
/* remove an item from buffer to next_consumed */
. . .
wait(full); // 다 찰 때까지 대기한다.
wait(mutex);
/* consume the itenm in next_consumed */
signal(mutex);
signal(empty); // 버퍼가 비었음을 알린다.
}
위의 방법에서 생산자는 아래와 같이 동작한다.
: 소비자가 버퍼를 전부 소모할 때까지 대기한다.
: 다 비우면 생산자가 동작한다.
: 생산자 여러 개가 움직이게 될 수도 있기 때문에 mutex
로 상호 배제한다.
자바의 코드도 아래 첨부한다.
public class BoundedBuffer {
public static void main(String[] args) {
CashBox cashBox = new CashBox(5);
Thread[] producers = new Thread[1];
Thread[] consumers = new Thread[1];
for (int i = 0; i<producers.length; i++) {
producers[i] = new Thread(new ProdRunner(cashBox));
producers[i].start();
}
for (int i = 0; i < consumers.length; i ++) {
consumers[i] = new Thread(new ConsRunner(cashBox));
consumers[i].start();
}
}
}
class ProdRunner implements Runnable{
CashBox cashBox;
public ProdRunner (CashBox cashBox) {
this.cashBox = cashBox;
}
@Override
public void run() {
try {
while(true) {
Thread.sleep((long)(Math.random()*500));
int money = ((int)(1 + Math.random()*9))*10000;
cashBox.give(money);
}
}catch(InterruptedException e) {}
}
}
class ConsRunner implements Runnable{
CashBox cashBox;
public ConsRunner (CashBox cashBox) {
this.cashBox = cashBox;
}
@Override
public void run() {
try {
while(true) {
Thread.sleep((long)(Math.random()*500));
int money = cashBox.take();
}
}catch(InterruptedException e) {}
}
}
class CashBox{
private int[] buffer;
private int count, in, out;
public CashBox(int bufferSize) {
buffer = new int[bufferSize];
count = in = out = 0;
}
synchronized public void give(int money) throws InterruptedException{
while(count == buffer.length) {
try {
wait();
}
catch(InterruptedException e) {}
}
buffer[in] = money;
in = (in + 1) % buffer.length;
count++;
System.out.println("여기있다, 용돈 : " + money + "원");
notify();
}
synchronized public int take() throws InterruptedException{
while(count == 0) {
try {
wait();
}
catch(InterruptedException e) {}
}
int money = buffer[out];
out = (out + 1) % buffer.length;
count --;
System.out.println("고마워유, 용돈 : " + money + "원");
notify();
return money;
}
}
(각각 동시성을 가지고 돌아가는 프로세스 간에 공유되는 데이터베이스를 생각해보자.)
일반적으로는 이 문제에 대해 reader-writer locks로 해결할 수 있다.
: 실행하려는 프로세스가 독자 프로세스인지, 작가 프로세스인지에 따라 락을 다르게 적용한다.
: 독자 프로세스가 락을 획득하면 모든 독자 프로세스에 관한 락은 풀고, 작가 프로세스에만 락을 적용한다.
: 작가 프로세스가 락을 획득하면 작가, 독자 모두에게 락을 적용한다.
결국 뮤텍스락에서 달라진건 독자 프로세스가 작업할 때도 다른 독자 프로세스 진입을 허용하는 것뿐이다.
독자-작가 문제에 대한 몇 가지 변형 예제를 들어보자.
모든 변형에는 우선 순위가 포함된다.
The first readers-writers problem
: 독자 프로세스는 자신 이외의 독자 프로세스가 종료되기 전까지 wait()
을 호출할 수 없다.
: 이 말은, 간단하게 얘기해서 작가 프로세스는 독자 프로세스 모두가 종료될 때까지 대기함을 의미한다.
: 만약 독자 프로세스가 계속해서 물밀듯이 들어오면 작가 프로세스는 기아(Starvation) 상태에 빠질 수 있다.
The second readers-writers problem
: 만약 작가 프로세스가 오브젝트에 접근하면, 새로운 독자 프로세스는 실행할 수 없다.
: 작가 프로세스에게 우선 순위를 높게 주어 먼저 실행하게 만드는 것이다.
: 이 역시, 작가 프로세스가 물밀듯이 들어올 경우 독자 프로세스가 기아 상태에 빠질 수 있다.
rw_mutx
는 독자, 작가 사이의 상호 배제를 위해 사용한다.mutex
는 공유 변수인 read_count
에 대한 상호 배제를 위해 사용한다.read_count
는 얼마나 많은 프로세스가 읽고 있는지 나타내기 위해 사용한다.read_count = 0 -> 작가 프로세스 진입
while(true) {
wait(rw_mutex); // 독자로부터 신호가 오면 진입
/* writing is performed */
signal(rw_mutex);
}
while(true) {
wait(mutex); // 공유 자원 접근을 위한 요청
read_count++; // 읽고 있는 프로세스의 수 더하기
if(read_count == 1)
wait(rw_mutex); // 독자 프로세스는 여러 프로세스가 진입이 가능하다.
signal(mutex);
/* reading is performed */
wait(mutex); // 공유 자원 접근을 위한 요청
read_count --;
if (read_count == 0)
signal(rw_mutex);
signal(mutex);
}
위의 코드를 보면 어째서 작가 프로세스가 기아 상태에 빠질 수 있는지 파악할 수 있을 것이다.
다시 정리해보자.
: 작가 프로세스가 임계 영역에 접근하면 많은 독자 프로세스는 waiting
상태에 빠질 것이다.
: 이 때 한 개의 독자 프로세스는 rw_mutex
에서, 그리고 나머지 독자 프로세스는 mutex
에서 대기하고 있을 것이다.
: 또한, 작가가 signal(rw_mutex)
연산을 실행할 때 대기중인 작가 프로세스를 실행할지, 독자 프로세스를 실행할지는 모두 스케줄러에 의해 결정된다.
: 어떤 프로세스의 우선 순위를 높이느냐에 따라 해결책이 갈리게 된다.
: 독자 프로세스의 우선 순위를 높인다면 첫번째 방법, 작가 프로세스의 우선 순위를 높인다면 두번째 방법이 될 것이다.
: 이에 대해 뭐가 좋다는 이야기 할 수 없고, 자신의 환경에 맞춰 설정해야 한다.
while (true) {
wait (chopstick[i]); // Left
wait (chopstick[(i+1) % 5]); // Right
/* eat for a while */
signal (chopstick[i]);
signal (chopstick[(i+1) % 5]);
/* think for a while */
}
wait()
연산을 실행하고, 젓가락을 놓을 때 signal()
연산을 실행한다.다들 왼쪽 젓가락을 잡고 오른쪽 젓가락을 달라고 서로 싸우는 꼴이 된다.
철학자, 즉 프로세스의 수를 제한할 수 있을 때 사용한다.
thinking
, hungry
, eating
로 구분한다.eating
상태가 아니어야 한다.hungry
상태이지만 음식을 먹을 수 없을 때 스스로를 지연시키도록 한다.
package Synchronization;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
enum State {
THINKING, HUNGRY, EATING
}
public class DiningPhilosophers {
public static void main(String[] args) throws Exception {
int numOfPhils = 5;
Philosopher[] philosophers = new Philosopher[numOfPhils];
DiningPhilosopherMonitor monitor = new DiningPhilosopherMonitor(numOfPhils);
for (int i = 0; i < philosophers.length; i++)
new Thread(new Philosopher(i, monitor)).start();
}
}
class Philosopher implements Runnable {
private int id;
private DiningPhilosopherMonitor monitor;
public Philosopher(int id, DiningPhilosopherMonitor monitor) {
this.id = id;
this.monitor = monitor;
}
@Override
public void run() {
while (true) {
think();
monitor.pickup(id);
eat();
monitor.putdown(id);
}
}
private void think() {
try {
System.out.println(id + ": Now I'm thinking.");
Thread.sleep((long) (Math.random() * 500));
}
catch (InterruptedException e) {
}
}
private void eat() {
try {
System.out.println(id + ": Now I'm eating.");
Thread.sleep((long) (Math.random() * 50));
}
catch (InterruptedException e) {
}
}
}
class DiningPhilosopherMonitor {
private int numOfPhils;
private State[] state;
private Condition[] self; // Condition Value
private Lock lock;
public DiningPhilosopherMonitor(int num) {
numOfPhils = num;
state = new State[num];
self = new Condition[num];
lock = new ReentrantLock();
for (int i = 0; i < num; i++) {
state[i] = State.THINKING;
self[i] = lock.newCondition(); // Condition
}
}
private int leftOf(int i) {
return (i + numOfPhils - 1) % numOfPhils;
}
private int rightOf(int i) {
return (i + 1) % numOfPhils;
}
private void test(int i) {
if (state[i] == State.HUNGRY && state[leftOf(i)] != State.EATING && state[rightOf(i)] != State.EATING) {
state[i] = State.EATING;
self[i].signal();
}
}
public void pickup(int id) {
lock.lock();
try {
state[id] = State.HUNGRY;
test(id);
if (state[id] != State.EATING) self[id].await();
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
public void putdown(int id) {
lock.lock();
try {
state[id] = State.THINKING;
test(leftOf(id));
test(rightOf(id));
} finally {
lock.unlock();
}
}
}
DiningPhilosopher
를 사용한다.pickup()
연산을 실행한다.putdown()
연산을 실행한다.