동시에 여러 개의 프로세스가 동일한 자료를 접근하여 경쟁하는 현상을 말한다.
Race condition이 일어나지 않기 위해
동기화 문제
1. Mutual Exclusion: P1이 critical section에서 돌고 있으면 다른 P들은 critical section에서 수행될 수 없다.
2. Progress: critical section에서 수행되고 있는 P가 없을 때, 이곳에서 수행되길 원하는 P들이 존재, 이 중 하나라도 critical section에서 곧바로 수행되도록 보장되어야 한다.(무제한 연기 No !)
3. Bounded Waiting: P 하나가 cs에 들어가길 원하는데 다른 P들이 들어갔다 나왔다를 반복하여 들어가길 원하는 P가 수행되지 못하는 경우가 생길 수 있기에 starvation을 방지하는 것이 필요하다. (A bound must exit)
// Process i
do {
flag[i] = true:
turn = j;
while(flag[j] && turn == j);
// critical section
flag[i] = false;
// remainder section
} while(true);
// Process j
do {
flag[j] = true:
turn = i;
while(flag[i] && turn == i);
// critical section
flag[j] = false;
// remainder section
} while(true);
다음은 프로세스 i와 j 2개가 임계구역에 들어가는 과정이다.
→ 현대 컴퓨터 구조는 기계어를 수행하기에 올바르게 수행된다는 보장은 없다.
Critical section problem은 공유 자원이 변경되는 동안 interrupt를 발생하지 않도록 하면 해결된다고 생각할 수 있지만, Multiprocessing 환경에서는 불가능하다는 것을 알 수 있다.
이런 환경에서 interrupt를 발생시키지 않으면 메시지가 모든 처리기에 전달되어야 하고 이는 상당한 시간을 소비하고 시스템 효율을 떨어뜨린다. → 현대 기계들은 인터럽트 되지 않는 특별한 Hardware 명령어를 제공하여 이를 간단히 해결한다.
원자적으로(atomic) 즉, 인터럽트되지 않는 명령어를 사용한다.
boolean test_and_set(boolean *target) {
boolean rv = *target;
*target = ture;
return rv;
}
// lock initialization: "false"
do {
while(test_and_set(&lock));
// critical section
lock = false;
// remainder section
} while(true);
1. test_and_set(&lock)은 현재의 lock 값(false)을 리턴한다. 단, lock 값은 true로 바뀌어있음. while문에서 빠져나오지 못하면 계속 true의 값을 가진다.(atomic)
→ **Mutual exclusion 만족**
2. lock의 초기값이 false이기에 처음에 임계구역에 진입한다.
3. 작업이 끝나면 lock을 false로 바꾸어주어 다른 프로세스가 while문을 빠져나올 수 있게 한다.
→ **Progress 만족**
- compare_and_swap 명령어 (atomic)
void compare_and_swap(int *value, int expected, int new_value) {
int temp = *value;
if(*value == expected) {
*value = new_value;
}
return temp;
}
// lock initialization: "0"
do {
while(compare_and_swap(&lock, 0, 1) != 0);
// critical section
lock = 0;
// remainder section
} while(true);
1. compare_and_swap(&lock, 0, 1)은 lock의 원래 값을 리턴한다. 단, lock이 0일 때만 1로 변경된다. 처음에 0인 lock의 값이 1로 바뀌어지고 임계구역으로 들어가면 이후 진입하려는 프로세스들은 1만 리턴하기에 while문을 빠져나오지 못한다.
→ **Mutual exclusion 만족**
2. 임계구역에 진입한 프로세스가 작업을 수행한 뒤 lock을 0으로 바꾼다.
3. 기다리던 프로세스가 while문을 빠져나오면서 임계구역에 진입한다.
→ **Progress 만족**
하지만 위 명령어 모두 Bounded waiting을 아직 만족하지 못한다.
한 프로세스가 lock의 값을 바꾸자마자 다시 진입이 가능하기 때문이다.
// waiting[], lock initialization "false"
do {
waiting[i] = true;
key = true;
while(waiting[i] && key) {
key = test_and_set(&lock);
}
waiting[i] = false;
// critical section
j = (i+1) % n;
while((j != n) && !waiting[j]) {
j = (j+1) % n;
}
if(j == i) lock = false;
else waiting[j] = false;
// remainder section
} while(true);
waiting 배열과 key라는 불린 변수가 추가되었다.
Mutual exclusion의 축약형태로 임계구역에 들어가기 전에 lock을 획득하고 빠져 나올 때 lock을 반환한다.
불린 변수 available를 통해 락의 가용 여부를 표시한다.
acquire() {
while(!available); // busy waiting
available = false;
}
release() {
available = true;
}
→ Busy waiting : 프로세스가 임계구역에 있는 동안 기다리는 다른 프로세스들은 acquire( ) 함수의 반복문을 계속 실행해야 한다.
lock이 사용될 수 있기를 기다리면서 프로세스가 계속 회전을 하고 있어 spinlock이라고도 한다. 이러면 다른 프로세스가 실행되지 못하기에 CPU 사이클 낭비를 초래하며, 위의 명령어 모두 이런 단점을 지니고 있다.
하지만 lock을 기다리는 동안은 context switching을 필요로 하지 않기에(프로세스가 상태가 변경되었다가 다시 돌아오는게 아닌 계속 실행 상태이기에) multiprocessing에서 spinlock이 사용된다.
→ 조금만 기다리면 바로 쓸 수 있다는 점을 이용해서 lock-unlock의 타임이 짧을 때 유용하다.
wait(S) {
while(S <= 0); // busy waiting
S--;
}
signal(S) {
S++;
}
S1; // Process 1
signal(S);
wait(S);
S2; // Process 2
S가 0으로 초기화 되어있다면 P1이 S1 작업 이후, S가 1로 증가될 때 비로소 P2가 S2 작업을 수행할 수 있게 된다.
프로세스를 대기 큐(wait queue)에 넣고 대기 상태로(waiting) 전환하여 다른 프로세스가 실행되도록 한다. → wait( )
임계구역을 나온 프로세스의 signal( ) 함수 호출에 따라 대기 상태인 프로세스가 준비 큐(ready queue)로 들어가 프로세스를 재시작하여 임계구역에 진입하게 된다.
block( )과 wakeup( )을 사용해 이를 해결한다.(링크드 리스트 사용)
```java
typedef struct {
int value;
struct process *list;
}semaphore;
void wait(semaphore *S) {
S->value--;
if(S->value < 0) {
// insert process into S->list.
block();
}
}
void signal(semaphore *S) {
S->value++;
if(S->value <= 0) {
// delete process from S->list.
wakeup(P);
}
}
```
block( ) 함수는 자기를 호출한 프로세스를 중지시킨다.
wakeup(P)은 봉쇄된 프로세스 P를 재시작한다.
pooling방식이 아닌 인터럽트 기반 방식으로 구현되며 빈번한 context switching이 발생한다.
queue를 만들기 위해 링크드 리스트를 사용
→ 임계구역이 짧으면 busy waiting, 길면 block & wake-up 방식을 사용한다.
P0은 P1이 Q를 놔주기를, P1은 P0이 S를 놔주기를 바랄 때를 말한다.
// P0 // P1
wait(S); wait(Q);
wait(Q); wait(S);
... ...
signal(S); signal(Q);
signal(Q); signal(S);
하나의 프로세스가 오랜기간 선택되지 못하여 실행되지 못 할 때를 말한다.
이는 프로세스가 임계구역을 기다릴 때 대기 큐에 들어가기 때문에 발생한다.
셋 이상의 우선순위를 가진 시스템에서만 발생한다.
우선순위가 P0 < P1 < P2이고 P2가 자원 R을 필요로 하고 R은 현재 P0이 사용하고있는 상황이다.
P2는 P0이 자원을 다 사용할 때까지 기다리는게 보통이지만, P1이 P0을 선점한다고 할 때 P2는 상대적으로 낮은 P1이 자월을 양도할 때까지 이를 더 기다려야 한다.
→ Priority-inheritance protocol
더 높은 우선순위 프로세스가 필요로하는 자원을 사용하는 프로세스에게 더 높은 우선순위를 부여한다. 위의 경우 P0이 임시적으로 P2의 우선순위를 상속받아 P1이 선점되는 것을 방지한다. (사용 후 원래의 우선순위로 돌아간다.)
// producer //consumer
do { do {
// produce an item
wait(empty); wait(full);
wait(mutex); wait(mutex);
// add an item to buffer // remove an item from buffer
signal(mutex); signal(mutex);
signal(full); signal(empty);
} while(true); // consume the item
} while(true);
Solution for problem 1.
Data set
// Reader
do {
wait(mutex); // 다른 reader는 여기서 대기
read_count++; // mutual exclusion
if(read_count == 1) // first reader
wait(rw_mutex); // writer가 없다는 것을 확실히 한다
signal(mutex); // release critical section
// reading is performed.
wait(mutex); //
read_count--;
if(read_count == 0) // no one is reading, 1이면 아직 읽는 중
signal(rw_mutex); // writers 원할 때 접근 가능
signal(mutex); // critical section 수행됨
} while(true);
```
```java
// Writer
do {
wait(rw_mutex);
// writing is performed.
signal(rw_mutex);
} while(true);
read_count가 감소하거나 늘어날 때는 wait(mutex)와 signal(mutex)로 감싸주고 reader가 있을 때는 wait(rw_mutex)로 writer의 접근을 막고 reader가 없을 때 signal(rw_mutex)를 통해 writer의 접근을 허용하게끔 한다.
5명의 철학자가 원형 테이블에 앉아 있고 테이블 중앙에는 밥이, 테이블에는 5개의 젓가락이 놓여 있다.(쌍이 아닌) 배고픈 철학자가 동시에 젓가락을 2개 집으면, 식사를 마칠 때까지 젓가락을 놓지 않고 식사를 한다.
→ deadlock과 starvation을 발생시키지 않고 여러 스레드에게 자원을 할당해야 할 필요를 표현한 것이다.
Semaphore를 사용할 때에도 특정 실행 순서로 진행되었을 때만 발생하는 타이밍 오류는 여전히 발생한다. (순서가 항상 일정하지 않기 때문)
Monitor는 항상 하나의 프로세스만이 실행되도록 한다. 이에 condition 변수를 추가하여 동시 접근과 동기화 문제를 해결한다.
monitor DiningPhilosophers {
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];
void pickup(int i) {
state[i] = HUNGRY; // 배고프면 waiting에 넣기
test(i);
if(state[i] != EATING) self[i].wait();
}
void putdown(int i) {
state[i] = THINKING;
test((i + 4) % 5); // 다 먹은 state가 오른쪽인지 확인
test((i + 1) % 5); // 다 먹은 state가 왼쪽인지 확인
}
void test(int i) { // 양쪽 state가 먹지 않는 상태이고 자신이 배고플 때 먹는다
if((state[(i + 4) % 5] != EATING && (state[(i + 1) % 5] != EATING)) {
state[i] = EATING;
self[i].signal();
}
init() {
for(int i = 0; i < 5; ++i) {
state[i] = THINKING;
}
}