
💡6장 목표
- Critical-section problem와 해결책
- 전통적인 Process Synchronization 문제
- Process Synchronization 해결 도구
프로세스 또는 thread는 동시에 실행될 수 있으며, 언제든지 interrupt가 발생할 수 있어 실행이 부분적으로 완료될 수 있다. 또한, 동시 실행은 공유 데이터에 대한 data inconsistency(데이터 불일치)를 초래할 수 있다. 데이터의 일관성을 유지하려면 협력 프로세스의 순차적 실행을 보장하는 메커니즘이 필요하다.
예를 들어, comsumer-producer문제를 해결하기 위해 모든 버퍼를 채운다고 생각하자. 전체 버퍼 수를 추적하기 위해 integer counter를 사용한다. 초기 counter는 0으로 설정하고, 생산자는 새로운 버퍼를 생산한 후 counter를 증가시키고, 소비자는 버퍼를 소비한 후 counter를 감소시킨다.
// producer
while (true) {
/* produce an item in next produced */
while (counter == BUFFER_SIZE)
; /* do nothing */
buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
// consumer
while (true) {
while (counter == 0)
; /* do nothing */
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
/* consume the item in next consumed */
}
counter를 증감하는 과정은 machine instruction관점에서 register를 사용한 여러 과정을 거친다. 이 과정에서 순서가 보장되지 않으면, counter의 값이 일관성을 잃을 수 있다. (아래 예시는 이상적으로 5의 값을 가져야할 counter가 실행 순서에 따라 4의 값을 갖게됨) → race condition
또 다른 Race condition의 예시는 아래와 같다.

이러한 상황을 방지하기 위해 한순간에 하나의 프로세스만이 data를 변경하도록 보장해야함

Critical-section problem에 대한 해결안은 아래 세 가지 조건을 충족시켜야 한다.
OS에 존재하는 critical-section은 커널이 선점형인지 비선점형인지에 따라 handling 방식이 다르다. 예를 들어, critical section이 보호되지 못하는 선점형의 경우는 Mutual Exclusion을 보장해주는 코드를 구현해야한다.
Critical-section을 보호하기 위한 여러 Solution에 대해 소개한다. 이 Solution들은 아래와 같이 분류할 수 있따.
software 기반 Peterson’s Solution hardware 기반 Uni Processor, Memory barrier, Hardware instruction, Atomic vairable hardware를 쉽게 사용할 수 있도록 도와주는 API Mutex Lock, Semaphore
critical section problem을 해결하는 고전적인 소프트웨어 기반 해결책이다. 현대 컴퓨터 구조에서 항상 올바르게 작동한다고 보장할 수 없다.
while (true){
flag[i] = true; // i프로세스가 들어갈 준비가 됨
turn = j; // 자신이 아닌 상태 프로세스로 설정
while (flag[j] && turn == j)
; /* critical section에 들어갈 수 없어 대기 */
/* critical section */
flag[i] = false;
/* remainder section */
}boolean flag = false;
int x = 0;while (!flag);
print x;x = 100;
flag = true;x에 100이 할당되기 전에 thread 1이 실행돼서 0이 출력됨 → race condition
flag = true;
x = 100;
이러한 문제는 두 프로세스가 동시에 critical section에 들어가도록 허락할 수 있다.

Peterson’s Solution과 같은 소프트웨어 기반 해결책은 현대의 컴퓨터 구조에서 올바른 작동을 보장하지 못하며 속도가 느리다. 많은 시스템은 critical section 구현을 위한 하드웨어를 지원한다. 하드웨어를 사용하는 해결책들은 critical section을 보호하기 위해 Lock을 사용한다는 점에 기반을 둔다.
single processor환경에서는 공유 변수가 변경되는 동안 interrupt 발생을 허용하지 않음으로써 해결한다.
하드웨어 지원에는 Memory barriers, Hardware instructions, Atomic variables의 세가지 형태가 존재한다.
while (!flag)
memory_barrier();
print x;x = 100;
memory_barrier(); // reorder 방지
flag = true;특정 변수의 값을 읽고, 그 값을 수정하는 Atomic 작업 수행
Definition
boolean test_and_set(boolean *target) {
boolean rv = *target;
*target = true;
return rv;
}
Solution
do {
while (test_and_set(&lock))
; /* do nothing */
/* critical section */
lock = false;
/* remainder section */
} while (true);
lock이 false이면 잠겨있지 않아서 critical section에 들어갈 수 있음을 의미한다. test_and_set에서는 false를 반환하고 lock의 값을 true로 바꿔 잠군다. while 루프에서 나와 critical section을 수행하고 난 뒤에는 lock을 false로 바꿔 잠금을 해제한다. critical section에 들어올 수 있는 상태로 만든다.
Definition
int compare_and_swap(int *value, int expected, int new_value) {
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
Solution 1
while (true) {
while (compare_and_swap(&lock, 0, 1) != 0)
; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
}
lock이 0이면 잠겨있지 않아서 critical section에 들어갈 수 있음을 의미한다. compare_and_swap에서는 0을 반환하고, *value(0)가 expected(0)과 동일하므로 value(0)를 new_value(1)로 설정한다. 즉, lock이 잠겨있지 않음을 확인하고, 1로 바꿔 잠구는 과정이다. while 루프에서 나와 critical section을 수행하고 난 뒤에는 lock을 다시 0으로 설정하여 잠금을 해제한다.
이 알고리즘은 Mutual Exclusion 조건은 만족하지만, Bounded Waiting을 만족하지 못한다…. 프로세스가 3개라면 p1이 cs를 탈출한 뒤 p2와 p3 중 어떤것을 선택할지 보장할 수 없기 때문이다.. 이를 만족시키기 위해 아래와 같은 알고리즘을 사용할 수 있다.
Solution 2: Bounded-waiting Mutual Exclusion(유한 대기 상호 배제)
while (true) {
waiting[i] = true;
key = 1;
while (waiting[i] && key == 1)
key = compare_and_swap(&lock, 0, 1);
waiting[i] = false;
/* critical section */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = 0;
else
waiting[j] = false;
/* remainder section */
}
첫 while문에 들어온 경우 critical section에 들어가려고 시도하는 것이므로 waiting을 true로 설정한다. 그러면 다음 while문에 들어가게 되고, lock이 false인 상태로 compare_and_swap을 호출한다. false가 반환되어 key는 0이 돼서 critical section에 들어오지 못하도록 잠기고, lock은 true로 바뀐다. 이제 들어온 프로세스는 기다리는 상황도 아니니까 waiting도 false로 바꾸고, critical section을 수행한다.
Pi가 critical section을 수행하고 나오면 i+1해서 j를 만든다. j에게 우선권을 주며 Pj가 waiting상태인지 확인한다. waiting상태가 아니라면 j+1해서 또 다음 노드로 넘어가 우선권을 준다. Pj가 waiting상태라면 lock을 false로 해서 잠금을 해제하고, waiting[j]를 false로 바꾸며 critical section에 들어갈 수 있도록 해준다.
결국 프로세스는 Lock==0&& waiting[i]==true && thread[i-1]이 cs를 나온 경우에 들어갈 수 있다는 것이다.
void increment(atomic_int *v)
{
int temp;
do {
temp = *v;
}
while (temp != (compare_and_swap(v,temp,temp+1));
} 위의 하드웨어 기반 해결책은 복잡한 형태를 가진다. 이를 프로그래머가 쉽게 사용할 수 있도록 간단한 API들이 개발되었으며, 대표적으로 Mutex Lock과 Semaphore가 있다.
while (true) {
acquire lock
/* critical section */
release lock
/* remainder section */
}
acquire() {
while (!available)
; /* busy wait */
available = false;
}
release() {
available = true;
} 하지만, Busy waiting을 해야한다는 단점을 가진다. 이는 한 프로세스가 critical section에 있는 동안 다른 프로세스들은 acquire()로 lock을 얻기 위해 계속해서 반복문을 호출한다(CPU사용권을 놓지 않음). lock이 풀리길 기다리며 계속 회전하므로 spinlock이라고도 부른다. 이는 multiprogramming system에서 CPU사이클을 낭비하게 만든다.
그러나, 이는 lock을 기다리는동안 시간을 소모하는 context switch를 전혀 필요로 하지 않는다는 장점을 가져다주기도 한다. 그래서 프로세스들이 짧은 시간 동안만 lock을 소유할 것이라고 예상되면, spinlock이 적절할 수 있다.
Mutex lock보다 더 정교한 방법을 제공하는 동기화 도구이다.
wait(S) {
while (S <= 0)
; // busy wait
S--; // 자원을 하나 사용하겠다
}
signal(S) {
S++; // 자원 사용을 끝마쳤다
}P1:
S1;
signal(synch);
// 작업 S1을 수행하고 semaphore를 증가하여 P2를 깨움(interrupt처럼)
P2:
wait(synch);
S2;
// signal interrupt 대기하다가 신호가 오면 작업 S2를 수행signal()과 wait()두 함수는 동시에 두 개의 프로세스가 실행할 수 없도록 보장해야한다. 이를 위해 두 함수는 critical section 내에서 실행되어야 한다. 하지만, 이는 Busy waiting을 하게 만든다. 다른 프로세스가 이미 critical section에 있을 경우 계속 기다려야한다.
그래도 두 함수 구현 코드 자체가 짧아서 busy waiting 시간이 짧을 수 있다. 그러나 critical section이 자주 차지되는 경우는 application은 많은 시간을 기다리게되고 CPU 사용권을 놓지 않아 성능 저하가 발생할 수 있다.
이를 해결하기 위해 아래와 같은 방법을 고려한다.
typedef struct {
int value; // 세마포어의 현재 값
struct process *list; // 대기 큐의 첫 번째 프로세스를 가리키는 포인터
} semaphore; wait(semaphore *S) {
S->value--; // 세마포어의 값을 감소시킴 (자원 사용)
if (S->value < 0) { // 사용할 자원이 없으면(크리티컬 섹션에 들어갈 수 없으면)
add this process to S->list; // 대기 큐에 현재 프로세스를 추가
block(); // 프로세스를 블록 상태로 전환
}
}
signal(semaphore *S) {
S->value++; // 세마포어의 값을 증가시킴 (자원 반납)
if (S->value <= 0) { // 반납해도 음수라는 것은 기다리는 애들이 있다는 것(크리티컬 섹션에 들어가고 싶은 애들이 있음)
remove a process P from S->list; // 대기 큐에서 프로세스를 제거
wakeup(P); // 제거한 프로세스를 준비 큐로 옮김
}
}// 1. 잘못된 순서로 Semaphore 연산 사용
signal(mutex);
. . .
wait(mutex);
// 2. 중복 호출
wait(mutex);
. . .
wait(mutex);
// 3. 생략
wait(mutex);
. . .
//signal(mutex) -> 생략


세마포를 사용해서 monitor를 구현하면 아래와 같다.
1. F함수 보호
2. 조건 변수 사용 (뭘 깨우고 어떤걸 기다리는지~)


보통 동기화 프로그램은 ‘기다리고-깨우고’하는 형태이다. 이때, 프로그램이 계속 실행되기 위해서 무한정 기다리는 상황이 발생하면 안된다. → Liveness failure
