Concurrency Control(병행 제어)라고도 한다.
Race Condition 상황을 살펴보기 전
컴퓨터 시스템에서 데이터를 어떻게 접근하는지 보자.
데이터가 저장되어 있는 위치로부터 데이터를 읽어와서 연산한 후, 연산 결과를 이전에 저장되어있던 그 위치에 다시 저장하게 된다.
Execution-Box(E-box)와 Storage-Box(S-box)의 예시를 보면 이해가 될 것이다.
E-box | S-box |
---|---|
CPU | Memory |
컴퓨터 내부 | 디스크 |
프로세스 | 그 프로세스의 주소 공간 |
데이터를 읽기만 하면 문제가 없는데, 데이터를 수정하게 되면 무엇이 먼저 읽었는지 중요하게 된다.
count++
) 도중에, 다른 곳에서 그 count를 가져가 1 감소시키면(count--
), (count--
) 의 결과만 반영된다.: 커널 모드 running 중 interrupt가 발생하여 인터럽트 처리 루틴이 수행
-> 양쪽 다 커널 코드이므로 kernel address space 공유
다음 그림을 봤을 때, 논리적으로는 Count 값이 원래 10이었다면 결과도 10이 되어야 한다. 10 -> 9 -> 10
or 10->11->10
)하지만 이 연산의 결과는 11이 된다.
Count--
이 되어 Count 값은 9가 될 것이다.Count++
수행 Count 값은 11이 된다.어떤 CPU가 마지막으로 count를 store 했는가 -> race condition
멀티프로세서의 경우에는 interrupt enable/disable로 해결되지 않는다.
정확히 개념을 이해하자!
각 프로세스의 code segment에는 공유 데이터를 접근하는 코드인 critical section이 존재한다. 공유 데이터를 접근하는 부분을 critical section이라고 한다.
👉 하나의 프로세스가 critical section에 있을 때 다른 모든 프로세스는 critical section에 들어갈 수 없어야 한다.
프로세스 Pi가 critical section 부분을 수행 중이면 다른 모든 프로세스들은 그들의 critical section에 들어가면 안 된다.
아무도 critical section에 있지 않은 상태에서 critical section에 들어가고자 하는 프로세스가 있으면 critical section에 들어가게 해줘야 한다. (아무도 없을 땐 들어오는 프로세스는 바로 들어가게 해줘야 한다.)
프로세스가 critical section에 들어가려고 요청한 후부터 그 요청이 허용될 때까지 다른 프로세스들이 critical section에 들어가는 횟수에 한계가 있어야 한다. (기다리는 시간이 무한이 되지 않도록 구현해야 한다.)
두 개의 프로세스가 있다고 가정하자 (P0, P1)
- 프로세스들의 일반적인 구조
do { entry section critical section exit sectiojn remainder section } while (1);
- 프로세스들은 수행의 동기화(synchronize)를 위해 몇몇 변수를 공유할 수 있다. (synchronization variable)
하나의 flag를 사용하는 알고리즘
두 프로세스 P0, P1 있다고 가정하자
int turn;
initially turn = 0; // Pi can enter its critical section if (turn == i)
do {
while (turn != 0); // My turn(turn == 0)이 될 때까지 반복
critical section
turn = 1; // Set P1 trun
remainder section
} while (1);
do {
while (turn == 0); // My turn(turn != 0)이 될 때까지 반복
critical section
turn = 0; // Set P0 trun
remainder section
} while (1);
이 알고리즘은 Mutual exclusion은 만족하지만,
Progress는 만족하지 않는다.
과잉 양보
ex) 만약 P0의 remainder section 부분이 너무 긴 경우, P1에서 turn 을 0으로 바꾸고 다시 while문을 반복할 때, 아무도 critical section에 수행하고 있는 프로세스가 없는데 Process가 진입을 하지 못한다.
하나의 flag를 사용하는 알고리즘
boolean flag[2];
initially flag[all] = false; // no one in CS(Critical Section)
// Pi ready to enter its critical section if (flag[i] == true)
do {
flag[i] = true; // Pretend I am in
while (flag[j]); // 상대 프로세스를 기다린다. (false가 될 때까지)
critical section
flag[i] = false; // I am out now
remainder section
} while (1);
Mutual exclusion은 만족하지만, Progress는 만족하지 않는다.
flag[i] = true;
까지 수행 후, context switch가 발생하는 경우, 끊임 없이 양보하는 상황이 발생가능하다.
Algorithm 1과 2의 synchronization variables를 결합했다.
do {
flag[i] = true; // My intention is to enter (cs에 들어가고 싶다.)
turn = j; // 상대 프로세스 먼저 Set
while (flag[j] && turn == j); // 2개 동시 만족하는 상황일 때만 기다림
critical section
flag[i] = false;
remainder section
} while (1);
이 알고리즘은 Mutual exlusion, Progress, Bounded waiting 모두 충족하지만
Busy Waiting 문제가 있다.
계속 CPU와 memory를 쓰면서 wait한다.
CMP flag
CMP turn
jmp start
-> 비효율적
하드웨어적으로 Test & Modify를 atomic하게 수행할 수 있도록 지원하는 경우 앞의 문제가 간단히 해결된다.
atomic : 하나의 operation인 것처럼 수행한다.
=> 위에서 문제가 생겼던 이유는 데이터를 읽는 것과 데이터를 쓰는 것을 하나의 Instruction으로 처리할 수 없었기 때문에 발생한 것이다.
(Ex. Count++ 연산 => 기계어로는 Load + Inc + Store 3단계로 구성)
// Synchronization variable
boolean lock = false;
// Process Pi
do {
while (Test_and_Set(lock));
critical section
lock = false;
remainder section
}
/*
Test할 때 lock이 false이면, 자기 프로세스 차례 -> lock = true로 Set
-> Test, Set이 하나의 어셈블리 명령어처럼 처리된다.
*/
Algorithm 2에서는 context-switch할 때 문제가 발생하는데, 이것은 문제가 생기지 않는다.
하지만 Busy Waiting 문제는 발생한다.
앞의 방식들을 추상화시킴(like oop)
// P(S)
while (S <= 0) do no-op; // 아무것도 하지 않는 조건
S--; // S가 0보다 크면 S--
/*
if 양수 : decrement & enter
Otherwise : wait until Positive(busy-wait)
*/
// V(S)
S++;
// Synchronization variable
semaphore mutex; // initially 1 : 1개가 CS에 들어갈 수 있다.
// Process Pi
do {
P(mutex); // +이면 -- & enter, 아니면 wait
critical section
V(mutex); // Increment semaphore
remainder section
} while (1);
운영체제가 2개의 API에 대해 atomic한 연산을 보장해주기 때문에, Algorithm 2의 문제가 발생하지 않는다.
Busy-Wait은 해결하지 못했다.
Busy Wait보다 더 잘 쓰인다.
typedef struct
{
int value; // semaphore
struct process *L; // process wait queue
} semaphore;
// P(S)
// wait(suspend) 시킬 수 있음.
S.value--; // cs에 들어갈 준비
if (S.value < 0) // 들어갈 수 없는 조건
{
add this process to S.L; // wait queue에 넣는다.
block(); // suspend
/*
나중에 필요할 때 wake up 하기 때문에
busy wait 상태 X, CPU 낭비 X
*/
}
// V(S)
// wakeup 시킬 수 있음
S.value++;
if (S.value <= 0) // LinkedList(wait queue)에 process가 대기하고 있음을 의미
{
remove a process P from S.L;
wakeup(P);
}
Block/wakeup overhead vs Critical section 길이