데이터가 저장되어 있는 위치(ex. memory, 디스크, 프로세스의 주소 공간)에서 데이터를 읽어와서 연산을 하고(ex. CPU, 컴퓨터 내부, 프로세스) 연산 결과를 원래 있던 위치에 저장함.
-> 2가지 이상의 연산 수정 결과를 동시에 원래 있던 위치에 저장하려고 할 때 동기화 문제 발생
2개 이상의 주체가 동시에 데이터를 접근하려고 할 때 경쟁 상태 발생 가능성이 있음.
공유 메모리를 사용하는 프로세스 간:
공유 데이터인 커널 내부 데이터 접근:
Multiprocessor 시스템:
-> race condition을 막기 위해서는 동시에 실행되는 프로세스(concurrent process)는 동기화되어야 한다.
do {
// entry section (lock)
// critical section
// exit section (unlock)
// remainder section
} while (1);
// synchronization variable
int turn
turn = 0; // Pi는 turn == i일 때 임계구역 진입 가능
// 프로세스 0 입장에서
do {
while (turn != 0); /* My turn?이 아니면 대기 */
// critical section
turn = 1; /* 프로세스 1아 이제 니 차례다 */
// remainder section
} while(1)
프로세스 0이 turn=1로 바꿔놓고 임계구역을 빠져나왔는데, 프로세스 1이 turn=0으로 바꿔주기 전까지는 임계구역에 진입하지 못함.
극단적으로, 프로세스 1이 처음에 한 번만 임계구역에 진입하고, 프로세스 0은 임계구역에 100번 진입해야 하는 경우, 프로세스 0은 한 번 진입 후 다시 임계구역에 진입하지 못할 수도 있음.
❓: 이렇게 극단적인 경우를 미루어볼 때 이 코드는 한정 대기 조건을 충족하지 못하는 경우 아닌가? 프로세스1이 turn=0로 안바꿔주면 프로세스 1이 한 번 진입 후 다시 임계구역에 진입하지 못하게 되니까?
Algorithm 2:
// synchronization variable
boolean flag[2];
flag[0], flag[1] = false, false; /* 아무도 임계구역에 없다. */
// Pi는 flag[i] = true일 때 임계구역 진입 가능
// 프로세스 0 입장에서
do {
flag[i] = true; /* CS에 진입하겠다는 의사 표시 */
// 이 때 CPU timeout이 된다면? 한정 대기 조건 미충족
while (flag[j]); /* 저 쪽이 이미 깃발을 들었으면 대기 */
// critical section
flag[i] = false;
// remainder section
} while(1)
Algorithm 3 (Peterson's Algorithm):
// synchronization variable
int turn
boolean flag[2];
turn = 0; // Pi는 turn == i일 때 임계구역 진입 가능
flag[0], flag[1] = false, false; /* 아무도 임계구역에 없다. */
// Pi는 flag[i] = true일 때 임계구역 진입 가능
// 프로세스 0 입장에서
do {
flag[i] = true; /* CS에 진입하겠다는 의사 표시 */
turn = j /* 재빨리 j 차례로 바꿔줌 */
while (flag[j] && turn == j); /* 두 프로세스가 동시에 flag를 true로 바꾸더라도, 한쪽이 turn을 상대방쪽으로 바꿔주면 상대가 CS 진입 가능 */
// critical section
flag[i] = false;
// remainder section
} while(1)
애초에 CPU instruction 각각 실행되는 도중에 CPU를 빼앗길 수 있기 때문이다.
-> 하드웨어적으로 instruction들을 하나로 묶어 atomic하게 처리하자!
// synchronization variable:
boolean lock = false;
do {
while (Test_and_Set(lock)); /* lock을 atmoic하게 true로 바꿈 */
// critical section
lock = false;
// remainder section
}
acuire() {
while (!available) /* busy wait */
avilable = false;
}
release() {
available = true;
}
Semaphores: 프로세스가 임계구역에 진입하기 전에 S를 atomic하게 감소시켜야 하고(P(S)), 임계구역에서 나올 때 S를 atomic하게 증가시켜야 한다.(V(S)), busy waiting(=spin lock)은 효율적이지 못하므로 block & wakeup의 방식으로 구현 가능하다. (=sleep lock)
카운팅 세마포어(counting semaphore)와 이진 세마포어(binary semaphore)로 구분된다.
wait(S) {
while (S <= 0); /* busy wait */
S--;
}
signal(S) {
S++;
}
세마포어는 추상자료형의 일종이다.
추상 자료형(ADT)
구현과 분리하여 자료형을 추상적으로 정의한 것. 구현 방법을 명시하지 않는다는 점에서 자료구조와 다르다.
S라는 integer variable이 있고, 이 변수에 대해 정의되는 연산 P, V가 있다. P, V도 atomic하게 연산이 수행됨을 가정하고 있음.
P(S): 공유자원 S의 획득:
while (S <= 0) no-op; // 양수가 될 때까지 대기 (busy waiting 문제 여전히 발생)
S--; // S가 양수이면 S-- 하고 진입
V(S): 공유자원 S의 반납
S++;
이 있다.
resource queue에서 sleep하며 CPU를 얻을 권한을 block하다가, 공유데이터가 반납되면 깨움
typedef struct
{
int value; /* semaphore */
struct process L; /* process wait queue */
} semaphore
P()와 V()의 구현
P(S):
S.value--;
if (S.value < 0) {
// add this process to S.L
// block();
}
V(S):
S.value++;
if (S.value <= 0) { // 양수였다면 sleep중인 프로세스를 깨울 일도 없으므로 if문에 진입하지 않음
// remove a process P from S.L
// wake up(P);
}
일반적으로는 CPU 사용률만 놓고 봤을 때 block/wakeup이 더 효과적임.
다만 block/wakeup의 경우, 프로세스의 상태를 바꾸는 데에 overhead가 존재하긴 한다. (자원이 부족하면 프로세스의 상태를 잠자는 상태로 바꿔야 함. 자원이 반납되면 프로세스의 상태를 ready 상태로 바꿔야 함)
둘 이상의 프로세스가 서로 상대방에 의해 충족될 수 있는 event를 무한히 기다리는 현상
이미 자원을 획득한 프로세스들이 잠자고 있는 프로세스가 필요한 자원을 release하지 않아, 잠자는 프로세스가 wakeup하지 못하는 현상.
// synchronization variable
semaphore chopsticks[5];
// 모든 젓가락이 1로 초기화되어 있음.
// Philosopher i
do {
P(chopsticks[i]); /* 왼쪽 젓가락 집기 */
// 모든 철학자가 동시에 실행하면 다음 코드 실행 불가 (deadlock)
// P(chopsticks[(i + 1) % 5]); /* 오른쪽 젓가락 집기 */
...
} while (1);
- Bounded-Buffer Problem (생산자-소비자 문제)
- Readers and Writers Problem
- Dining-Philosopher Problem
이미지 출처: https://pages.mtu.edu/~shene/NSF-3/e-Book/SEMA/TM-example-buffer.html
버퍼: 임시로 데이터를 저장하는 공간
공유 버퍼 전체
에 lock을 걸기 -> need binary semaphore (공유 데이터의 상호 배제를 위해)pseudo code (생산자 코드와 소비자 코드가 대칭임)
// synchronization variable
semaphore full = 0, empty = n, mutex = 1;
// producer
do {
...
produce an item in x
...
P(empty)
P(mutex)
...
add x to buffer
...
V(mutex)
V(full)
} while (1);
// consumer
do {
P(full)
P(mutex);
...
remove an item from buffer to y
...
V(mutex)
V(empty)
...
consume the item in y
...
} while(1);
이미지 출처: https://www.zephyrproject.org/common-multithreading-problems-and-their-fixes-part-3/
pseudo code
//shared data
int readcount = 0;
DB 자체;
// synchronization variables (mutex는 공유 변수 readcount를 접근하는 코드(임계 구역)의 상호 배제 보장을 위해 사용 - readcount에 lock을 거는 용도의 이진 세마포어,
// db는 Reader와 Writer가 공유 DB 자체를 올바르게 접근하게 하는 역할 - DB 자체에 lock을 거는 용도의 이진 세마포어)
semaphore mutex = 1, db = 1;
// writer
P(db);
...
writing
...
V(db);
// Reader
P(mutex);
readcount++;
if (readcount == 1) P(db); /* block writer */
V(mutex); /* readers follow */
...
reading DB is performed
...
P(mutex);
readcount--;
if (readcount == 0) V(db); /* enable writer */
V(mutex);
이미지 참조: https://www.geeksforgeeks.org/dining-philosopher-problem-using-semaphores/
배가 고파지면 왼쪽과 오른쪽 젓가락을 집어서 밥을 먹고, 배가 부르면 젓가락을 내려놓고 생각하는 것을 무한 반복.
젓가락은 공유자원이고 1로 초기화되어있기 때문에, 만약 잡을 수 없다면 기다려야 함.
// synchronization variable
semaphore chopsticks[5];
// 모든 젓가락이 1로 초기화되어 있음.
// Philosopher i
do {
P(chopsticks[i]); /* 왼쪽 젓가락 집기 */
P(chopsticks[(i + 1) % 5]); /* 오른쪽 젓가락 집기 */
...
eat();
...
V(chopsticks[i]); /* 왼쪽 젓가락 내려놓기 */
V(chopsticks[(i + 1) % 5]); /* 오른쪽 젓가락 내려놓기
...
think();
...
} while (1);
해결 코드
enum { thinking, hungry, eating } state[5];
semaphore self[5] = 0; /* 젓가락을 잡을 수 있는지 여부를 나타내는 이진 semaphore */
semaphore mutex = 1; /* state에 lock 거는 용도의 semaphore */
// philosopher i
do {
pickup(i);
eat();
putdown(i);
think();
} while(1)
void pickup(int i) {
P(mutex);
state[i] = hungry;
test(i); /* 양쪽 젓가락을 집을 수 있는지 체크 */
V(mutex);
P(self[i]); /* 양쪽 젓가락을 집을 수 없으면 wait, 집을 수 있으면 self[i]를 다시 0으로 만들고 eat()하러 감 */
void test(int i) {
if (state[(i + 4) % 5] != eating && state[i] = hungry && state[(i + 1) % 5] != eating) {
state[i] = eating;
V(self[i]); /* 젓가락을 집을 수 있는 상태로 바꿔줌 (자원을 1만큼 늘림) */
}
}
void putdown(int i) {
P(mutex);
state[i] = thinking;
test((i + 4) % 5) /* 왼쪽 철학자가 젓가락을 집을 수 있도록 알림을 해주는 역할 */
test((i + 1) % 5) /* 오른쪽 철학자가 젓가락을 집을 수 있도록 알림을 해주는 역할 */
V(mutex)
V(mutex)
Critical Section
P(mutex)
// mutual exclusion 깨짐 (lock이 제대로 안 걸림)
P(mutex)
Critical Section
P(mutex)
// Deadlock 발생 (lock이 반환이 안됨)
프로그래밍 언어 차원에서 동기화 문제를 해결.
동시 수행 중인 프로세스 사이에서 ADT의 안전한 공유를 보장하기 위한 high level synchronization constructor임.
monitor 내부의 procedure를 통해서만 공유 데이터에 접근할 수 있음
monitor monitor-name
{ shared variable declarations
procedure body P1 (...) {
...
}
procedure body P2 (...) {
...
}
procedure body Pn (...) {
...
}
{
initialization code
}
}
모니터 내에서는 한번에 하나의 프로세스만이 활동 가능
프로그래머가 동기화 제약 조건을 명시적으로 코딩할 필요가 없음
(원천적으로 procedure는 여러개가 동시에 실행될 수 없음
-> semaphore와 달리 개발자가 동시 접근을 막기 위해 lock을 걸 필요가 없음. 모니터가 알아서 하나의 프로세스만 공유데이터에 접근하게끔 해줌.)
단, 세마포어에서처럼 모니터에서도 자원의 개수가 0 이상일 때에만 임계구역에 접근하고 그렇지 않을 때에는 suspend하게끔 하는 코드가 필요.
condition x, y;
x.wait()
x.wait()을 호출한 프로세스는 다른 프로세스가 x.signal()을 invoke하기 전까지는 suspend된다.
x.signal
x.signal은 정확하게 하나의 suspend된 프로세스를 resume한다. suspend된 프로세스가 없으면 아무 일도 일어나지 않는다.
생산자 소비자 문제도 monitor를 이용하여 lock 없는 코드로 개선 가능.
pseudo code
// monitor bounded_bufffer
{ int buffer[N];
condition full, empty; /* condition variable은 값을 가지지 않고 자신의 큐에 프로세스를 매달아서 sleep 시키거나 큐에서 프로세스를 깨우는 역할만 함. */
void produce (int x)
{ if there is no empty buffer
// 비어있는 버퍼를 기다리는 줄에 줄서도록 block
empty.wait();
add x to an empty buffer
// 내용이 들어있는 버퍼가 없어서 잠들었던 소비자를 깨움
full.signal();
}
void consume (int *x)
{ if there is not full buffer
// 내용이 들어있는 버퍼를 기다리게 함
full.wait();
remove an item from buffer and store it to *x
// 내용이 비기를 기다리느라 잠들었던 생산자를 깨움
empty.signal();
}
}
식사하는 철학자 문제도 monitor를 이용하여 lock 없는 코드로 개선 가능
Each Philosopher:
{
pickup(i);
eat();
putdown(i);
think();
} while(1);
monitor dining_philosopher
{
enum {thinking, hungry, eating} state[5];
condition self[5];
void pickup(int i) {
state[i] = hungry;
test(i)
if (state[i] != eating) self[i].wait(); /* test 내부에서 if문에 진입 못했으면 못 먹으니까 기다려야 함. */
}
void putdown(int i) {
state[i] = thinking;
test((i + 4) % 5)
test((i + 1) % 5)
}
void test(int i) {
if (state[(i + 4) % 5] != eating && state[i] == hungry && state[(i + 1) % 5] != eating){
state[i] = eating;
self[i].signal(); /* 못 먹고 기다리고 있는 상태였으면 깨움 */
}
}
void init() {
for (int i = 0; i < 5;i++) state[i] = thinking;
}
}