동기화의 고전적 문제들
동기화의 고전 문제 3가지
- Bounded-buffer problem
- Readers and writers problem
- Solution 1
- Dining philosophers problem
- Solution 1
- Solution 2
Bounded-buffer problem
- N개의 버퍼에 여러 생산자와 여러 소비자가 접근
- 생산자(producer) – 한번에 하나의 아이템을 생산해 버퍼에 저장
- 동작 조건: 버퍼 내에 저장할 공간이 있는가?
- Buffer의 상태가 full이면 대기
- 동작 조건: 버퍼 내에 저장할 공간이 있는가?
- 소비자(consumer) – 버퍼에서 하나의 아이템을 가져옴
- 그리고 버퍼에서 삭제
- 동작 조건: 버퍼 내에 소비할 아이템이 있는가?
- Buffer의 상태가 empty이면 대기
- 생산자와 소비자가 충돌없이 버퍼를 접근할 수 있도록
- 버퍼 – critical section
Bounded-buffer problem 도식
Bounded-buffer problem 구현
- Buffer의 구현
- 1차원 배열로 구현
- Boolean Buffer[n];
- 초기화
- Buffer[0…n-1] = empty
- 생산자 operation
- Buffer 배열 중, empty인 버퍼 찾아 full로 바꿈
- Buffer[m] = full
- 소비자 operation
- Buffer 배열 중, full인 버퍼 찾아 empty로 바꿈
- Buffer[m] = empty
Bounded-buffer problem 세마포
- 문제 해결을 위한 세마포
- Empty: 버퍼 내에 저장할 공간
- 생산자의 진입을 관리
- Full: 버퍼 내의 소비할 아이템
- 소비자의 진입을 관리
- Mutex: 버퍼에 대한 접근을 관리
- 생산자, 소비자가 empty, full 세마포를 진입할 경우, buffer의 상태 값을 변경하기 위한 세마포
- Empty: 버퍼 내에 저장할 공간
- 세마포 value의 초기값
- full = 0
- counting 세마포
- empty = n // buffer에 empty인 entry가 n개
- counting 세마포
- mutex = 1
- binary 세마포
- full = 0
생산자 프로세스
do {
...
wait(empty); // 버퍼에 적어도 하나의 공간이 생기기를 기다림
wait(mutex); // critical section에 진입하기 위해 기다림
...
produce an item in nextp
nextp++;
...
signal(mutex); // critical section에서 빠져나왔음을 알림
signal(full); // 버퍼에 아이템이 있음을 알림
} while(1);
소비자 프로세스
do {
wait(full); // 버퍼에 적어도 하나의 아이템이 채워지기를 기다림
wait(mutex);
...
remove an item from buffer;
nextc++;
...
signal(mutex);
signal(empty); // 버퍼에 하나의 빈 공간이 생겼음을 알림
...
} while(1);
Q : 세마포를 wait, signal 말고 이외의 기능을 할 수록 설계할 수도 있나요? A : 안된다.
Q : bounded buffer problem에서는 그러면 read와 write 모두 mutex라는 binary 세마포를 잡을 수 있을 때만 critical section에 들어갈 수 있기 때문에 concurrency가 없다고 보면 되나요? A : mutex라는 binary 세마포에 의해 concurrency가 제한되어있다고 할 수 있다. 이를 개선하기 위해서는 버퍼의 모든 자리에 각각의 세마포를 두거나 등등 다양한 방법이 있을 것이다.
동기화 알고리즘 설계하는 methodology
- 동기화 문제를 서술
- 동작 조건을 명시하면서
- 사용할 세마포를 설계
- 몇 개를 사용할지
- 앞에 Bounded-Buffer problem은 세마포 1개로도 구현이 가능하다. mutex, full, empty를 커버하는 마스터 세마포를 두고, 나머지를 모두 critical section으로 취급하는 것이다.
- 그러나 이 방식은 concurrency가 배제된다. counting 세마포가 있던 지점이 모두 하나의 binary 세마포 안의 critical section이 되기 때문이다.
- 앞에 Bounded-Buffer problem은 세마포 1개로도 구현이 가능하다. mutex, full, empty를 커버하는 마스터 세마포를 두고, 나머지를 모두 critical section으로 취급하는 것이다.
- 무엇을 protect할지 고려
- 몇 개를 사용할지
- 알고리즘 구현
- 위 세마포를 사용하여 pseudo-code로 구현한다.
- Deadlock 대하여 고려한다.
- 검증
- 초기 값, starvation 체크
- starvation : 특정 프로세스의 우선 순위가 낮아서 원하는 자원을 계속 할당받지 못하는 상태, 기아 상태
- 초기 값, starvation 체크
Readers-Writers Problem
- 여러 Readers와 Writers가 하나의 공유 데이터를 읽거나 쓰기 위해 접근
- Readers - 공유 데이터를 읽는다.
- 여러 Reader는 동시에 데이터를 읽을 수 있다.
- Writers - 공유 데이터에 쓴다.
- Writer가 데이터를 수정하기 위해서는, reader 혹은 다른 writer가 작업을 하고 있지 말아야 한다.
Readers-Writers Problem 도식
Bounded-Buffer Problem과의 차이점
Bounded-Buffer에서 소비자는 reader가 아니라 writer였으므로 reader는 존재하지 않았지만 Readers-Writers에서는 Reader가 엄연히 존재하며 이들에 대해서는 mutex 만족할 필요가 없다.
Readers-Writers Problem Solution1
- 문제 해결을 위한 자료구조와 세마포
- int readcount : 버퍼에 현재 접근하는 Reader 숫자
- semaphore wrt : Writers간 동기화를 위한 세마포
- semaphore mutex : Readcount와 wrt의 접근이 원자적으로 수행됨을 보장하기 위한 세마포
- 초기값
- mutex = 1, wrt = 1, readcount = 0
Solution 1 - writer
wait(wrt); // entry section
...
writing is performed
...
signal(wrt); // exit section
Solution 1- reader
wait(mutex);
readcount++;
if (readcount == 1)
wait(wrt); // writer 들어오지 못하게 함
signal(mutex);
...
reading is performed
...
wait(mutex);
readcount--;
if (readcount == 0)
signal(wrt);
signal(mutex);
Q : writer가 공유 데이터에 접근했을 때 reader의 접근을 막기 위해 mutex를 잡아야하지 않나요? A : 아니다. reader가 처음으로 들어올 때 writer가 이미 wait(wrt)를 한 상태라면, 들어오지 못한다.
문제점
- Writer의 starvation
- Readcount == 0일 때만 signal(wrt)가 수행되어 wait(wrt)로 대기하고 있던 writer가 수행될 수 있음
- 첫 번째 Reader(Readcount == 1)가 wait(wrt)를 통과하면, 다른 Reader들은 wait(mutex)에 대한 signal(mutex)만 통과하면 writer에 관계없이 계속해서 진입할 수 있다.
- reader preference가 있음
- 여러 Reader들이 계속해서 진입할 경우, Readcount는 0까지 떨어지지 않을 수 있다.
- Writer의 starvation을 예방하는 solution이 존재함
Q : Writer의 starvation을 방지하는 방법에 대해 생각해봤는데, readcount = 10 과같이 초기화하여 reader process가 한번 접근할때마다 readcount– 하는 형식으로 풀수 있을까요? A : 좋은 아이디어지만, 10과 같이 인위적인 숫자를 넣지 않는 것이 좋은 알고리즘이다.
Dining-Philosophers Problem
- 고전적인 동기화 문제
- 5명의 철학자가 한 식탁에서 함께 식사를 하는 상황
- 젓가락이 5개뿐이다
- 자신의 바로 옆 젓가락만 집을 수 있다
- 두 젓가락을 모두 집어야 식사를 할 수 있다.
- 식사를 하고 난 다음에 두 젓가락을 모두 내려놓는다.
- Deadlock과 Starvation이 발생할 수 있음
- 모두들 자신의 오른쪽 젓가락을 동시에 집었을 경우
Dining-Philosophers Problem Solution1
-
젓가락을 집을 때 동기화를 하는 방법
- 자신의 왼쪽과 오른쪽 젓가락부터 집고 식사를 한다.
-
세마포
- chopstic[5] : 각 젓가락에 대한 동기화
-
초기값은 모두 1
-
철학자 프로세스
do { ... think ... wait(chopstick[i]) wait(chopstick[(i+1)%5]) ... eat // critical section ... signal(chopstick[(i+1)%5]) signal(chopstick[i]) ... } while(1);
-
동시에 chopstick[i]을 잡으면 deadlock 발생
- 모두가 왼쪽 손에 젓가락을 집고 오른쪽을 기다리고 있는 채로 무한정 대기하는 경우
Dining-Philosophers Problem 개선안
- Deadlock의 해결방안들
- 한 번에 최대 4명의 철학자만 식탁에 앉도록 한다.
- 젓가락의 상태를 미리 검사하여 양 쪽의 젓가락이 모두 사용 가능할 때만 젓가락을 집도록 한다.
- 철학자에게 번호를 부여하여 홀수인 철학자는 왼쪽의 젓가락을, 짝수인 철학자는 오른쪽의 젓가락을 먼저 집도록 한다.
- 위의 해결방안들은 starvation까지 해결해주지는 못함
- 각각의 방안에 대해 starvation에 대한 고려를 추가할 수 있다.
- 한 차례 굶은 철학자에게 우선권을 주는 방안
- 각각의 방안에 대해 starvation에 대한 고려를 추가할 수 있다.
Dining-Philosophers Problem Solution 2
- 양쪽의 젓가락을 한꺼번에 집는 방법
- 젓가락의 상태를 미리 검사하여 양 쪽의 젓가락이 모두 사용 가능할 때만 젓가락을 집도록 하는 방법
- 사용되는 자료구조
- State[5] : 각 철학자의 상태(THINKING, HUNGRY, EATING)
- 문제 해결을 위한 세마포
- mutex : 젓가락을 집거나 놓는 수행을 critical section으로 관리하기 위한 세마포 - 초기값: 1
- self[5] : 각각 젓가락 두 개를 잡는 세마포
- 초기값: 0
- self[i] 의미는 철학자 i가 HUNGRY 상태이더라도, 다른 젓가락 하나를 사용할 수 없을 경우 waiting을 하기 위한 세마포
- 철학자 프로세스
do {
...
think
...
take_chopsticks(i);
...
eat // critical section
...
put_chopsticks(i);
...
} while(1);
-
take_chopsticks
take_chopsticks(int i) { wait(mutex); state[i] = HUNGRY; test(i); signal(mutex); wait(self[i]); }
- Mutex를 통해 진입하여, test(i)를 통해 양쪽의 철학자 상태를 검사한 후, 자신이 먹을 차례를 기다린다.
-
put_chopsticks
put_chopsticks(int i) { wait(mutex); state[i] = THINKING; test(LEFT); test(RIGHT); signal(mutex); }
- Mutex를 통해 진입하여 test(LEFT), test(RIGHT)를 통해 양쪽의 철학자 상태를 검사한 후, 먹을 차례를 기다리는 철학자에게 signal을 보내준다.
-
test
test(int i) { if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; signal(self[i]); } }
해설
- Solution2
- 철학자 좌우 젓가락이 사용 가능할 때 critical section에 진입
- 즉, i번째 철학자가 식사를 하기 위해서는 i-1(LEFT)과 i+1(RIGHT) 철학자가 EATING 상태가 아니어야 한다.
- 철학자 좌우 젓가락이 사용 가능할 때 critical section에 진입
- take_chopstick()과 put_chopstick()의 동작을 구체적으로 살펴보자
take_chopstick()
take_chopsticks(int i)
{
wait(mutex);
state[i] = HUNGRY;
test(i);
signal(mutex);
wait(self[i]);
}
- 식사할 조건
- Test() 함수 안에서 검사하는 LEFT와 RIGHT의 상태가 EATING이 아니어야 한다.
- Test()를 만족하면 signal(self[i])에 의해 self[i]의 값은 1이 되므로 wait(self[i])에서 block되질 않고, 식사를 진행한다.
- self[i]의 초기값은 0임을 기억할 것
put_chopstick()
put_chopsticks(int i)
{
wait(mutex);
state[i] = THINKING;
test(LEFT);
test(RIGHT);
signal(mutex);
}
- 식사를 마치면, 좌, 우 철학자의 test() 함수를 실행한다.
- 이 때, 철학자 L과 R은 i에 의해 take_chopsticks()에서 wait() 함수에 의해 대기중
- 철학자 i에 의해 실행한 test(LEFT)
- i와 LL의 상태를 확인
- 철학자 i가 실행한 test(RIGHT)
- i와 RR의 상태를 확인
- 철학자 i가 식사중인 동안 LL 혹은 RR 철학자의 상태가 EATING으로 바뀐다하더라도, test(LEFT), test(RIGHT)에서 LL과 RR의 상태를 확인 후 signal을 수행하므로 동기화에 문제가 없다.
Q : 갑자기 햇갈려서 그런데 수업때 하는 동기화의 전제조건은 single core가 맞나요?? A : 이런 알고리즘은 싱글코어이든 멀티코어이든 상관없이 항상 critical section에는 하나의 프로세스만 들어가도록 해야 한다.
동기화 알고리즘 체크 요소들
- Data consistency(mutex)가 확보되는지
- Deadlock이 발생하는지
- Starvation 가능성이 있는지
- Concurrency를 얼마나 제공하는지
- Global lock을 쓰면 문제는 간단하게 풀리지만, concurrency가 심하게 제한된다.
Comments