이화여자대학교 컴퓨터공학과 반효경 교수님의 "운영체제 (KOCW)" 강의를 필기한 내용입니다.
다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.
문서를 이전하는 과정에서, 이미지 데이터가 유실되어 문서상에 보이지 않습니다.
Concurrency, Race Condition
(사진 사라짐)
- 일반적으로는 위 그림처럼 데이터를 저장하는 곳하고 연산하는 곳하고는 분리되어 있으며
- 연산하는 곳에서 데이터를 읽어들여 연산한 다음 저장하는 방식으로 작동되는데
(사진 사라짐)
- 위 그림처럼 동일한 데이터에 여러 연산이 접근하게 되면 문제가 생길 수 있다
- 이렇게 여러 연산이 하나의 데이터에 동시에 접근하는 문제를 Concurrency Problem, 동시성 문제 라 부르고
- 동시성 문제가 발생하게 되는 상황을 연산간 경쟁한다는 의미로 Race Condition 이라고 부르더라
- 이를 해결하기 위해서는 데이터의 접근 순서를 제어하는 로직이 필요하고 이런걸 Process Synchronization (프로세스 동기화) 라고 한다.
Common Race Condition Situations
- 커널 데이터
-
일반 프로세스의 경우에는 자신만의 메모리 공간이 있기 때문에 동시성 문제 잘 발생하지 않지만
-
커널의 경우에는 여러 프로세스가 Syscall 등을 이용해 공유할 수 있고
(사진 사라짐)
- 위 처럼 프로세스가 Syscall 을 해 커널모드에서 실행되다가 타임아웃이 난 후에 다른 프로세스로 넘어갔다가 여기서도 Syscall 을 걸어 커널 데이터를 변경하는 경우에 동시성 문제가 생길 수 있다
- 이때는 커널모드일때는 CPU 를 Preempt 하지 못하게 하고 커널모드가 끝나야 빼앗을 수 있게 함으로써 해결할 수 있다
-
커널모드에서 작업을 하다가 인터럽트가 걸리면 하던걸 멈추고 또 다른 커널 작업인 인터럽트 핸들링을 하게 되므로 이런 경우에도 문제가 생긴다
(사진 사라짐)
- 위 그림이 그 예시인데
- 이러한 경우는 커널 모드 실행중일때는 인터럽트가 걸리지 않게 하는 방식으로 해결할 수 있다
-
- 공유 메모리, 쓰레드
- 일반 프로세스에서 동시성 문제가 발생하는 경우 중 제일 흔한거는
- 프로세스 간 공유 메모리를 할당받았거나
- 멀티쓰레드 프로그래밍을 할 때이다
- 멀티쓰레드의 경우에는 쓰레드 간 메모리가 공유되기 때문에 동시성 문제가 생길 수 있다
Handling Concurrency
Critical Section
(사진 사라짐)
- 코드 상에서 공유 데이터 공간에 접근하는 부분을 Critical Section 이라고 한다.
- 그리고 Entry / Exit Section 에서 Critical Section 에 들어가는 프로세스들을 Lock 을 거는것처럼 관리하게 된다.
- 별로 중요한건 아니지만 공유데이터에 접근하지 않는 부분을 Remainder Section 이라고 한다
충족해야 할 조건들
- Mutual Exclusion: 상호 배제 → 하나의 프로세스가 Critical section 에 들어가 있으면 다른 프로세스는 들어가서는 안된다
- Progress: 현재 Critical section 에 들어가있는 프로세스가 없다면 Critical section 에 들어가고자 하는 프로세스는 거기 에 들어갈 수 있어야 한다.
- Bounded Waiting: 다른 프로세스가 Critical section 에 들어가 있어서 나머지 프로세스가 대기해야 한다면, 대기 시간이 유한해야 한다.
- 즉, 하나의 프로세스가 들어가서 빠져나오지 않는 상황이 발생하거나
- 특정한 몇개의 프로세스만이 Critical section 에 접근하여 나머지 프로세스들은 들어갈 수 없는 상황 (뭐 예를 들면 두개의 프로세스가 번갈아가며 들어가 다른 프로세스가 접근할 수 없는 상황) 이 되면 안된다.
Algorithm 1
- 프로세스 0 번의 코드가 다음과 같고
// global variable: int turn = 0;
do {
while (turn != 0);
critical_section();
turn = 1;
remainder_section();
} while (1);
- 프로세스 1 번의 코드가 다음과 같다면
// global variable: int turn = 0;
do {
while (turn != 1);
critical_section();
turn = 0;
remainder_section();
} while (1);
- 일단 Mutual Exclusion 은 달성할 수 있다
- 0번 프로세스는
turn
이 0이 될 때까지 기다리고 - 1번 프로세스는
turn
이 1이 될 때까지 기다리기 때문에 - 두놈이 같이 들어가는 상황은 막을 수 있음
- 0번 프로세스는
- 하지만 Progess 는 안된다
- 왜냐면 한놈이 Critical section 에 들어가는 것이 다른놈에게만 의존하기 때문에 한놈이 안드가게 되면 다른놈도 들어가지 못한다
- 가령 1번이 들어가려면
turn
값이 1이어야 되는데 0번이 들어가지 않은 경우에는turn
값이 0으로 남아있어 1번이 절대 들어가지 못하게 된다
Algorithm 2
- 이번에는 프로세스 0 번의 코드가 다음과 같고
// global variable: boolean flag[2] = {false, false};
do {
flag[0] = true;
while (flag[1]);
critical_section();
flag[0] = false;
remainder_section();
} while (true);
- 프로세스 1 번의 코드가 다음과 같다면
// global variable: boolean flag[2] = {0, 0};
do {
flag[1] = true;
while (flag[0]);
critical_section();
flag[1] = false;
remainder_section();
} while (true);
- 이번에도 Mutual Exclusion 은 달성할 수 있다
- 서로의
flag
가 올라가있는지 체크하면서 대기하기 때문에 critical_section()
에는 한번에 한놈만 드갈 수 있다.
- 서로의
- 그리고 Algorithm 1 에서의 문제점도 해결할 수 있다
- 프로세스가 연속해서 들어가고싶어할 경우에도 상대방의
flag
는 계속 false 이기 때문에 문제되지 않는다
- 프로세스가 연속해서 들어가고싶어할 경우에도 상대방의
- 하지만 이 경우에도 Progress 가 해결되지는 않는다
- 그건 Context switch 때문인데
- 프로세스 0번이
flag[0]
을true
로 바꾼 다음에 Contect switching 이 일어나서 - 프로세스 1번이
flag[1]
을true
로 바꾼다면 - 지금 아무도 드가있지 않지만 둘 다
true
로 되어 있어 아무도 들어가지 못하는 상황이 됨
Algorithm 3 (Peterson’s algorithm)
- 이번에는 프로세스 0 번의 코드가 다음과 같고
// global variable: int turn = 0;
// global variable: boolean flag[2] = {false, false};
do {
flag[0] = true;
turn = 1;
while (flag[1] && turn == 1);
critical_section();
flag[0] = false;
remainder_section();
} while (true);
- 프로세스 1 번의 코드가 다음과 같다면
// global variable: int turn = 0;
// global variable: boolean flag[2] = {0, 0};
do {
flag[1] = true;
turn = 0;
while (flag[0] && turn == 0);
critical_section();
flag[1] = false;
remainder_section();
} while (true);
- 보면 Algorithm 1 과 Algorithm 2 를 합쳐놓은 느낌인데 이 경우에는 모든 경우의 수를 만족할 수 있다
- 하나하나 따져보는 건 나중에 시간 많을때 해보고 그냥 느낌만 잡자면
while
문에서 상대방의flag
를 검사하기 때문에 일단 두명이 같이 드가는 것은 불가능하고- 만일 Algorithm 2 에서처럼 둘 다
flag
가 올라가있는 경우에는turn
값을 이용해 한놈은 드갈 수 있게 해주는 방식이다 - 하지만 이 방식은 작동은 하지만 다소 비효율적이다 → Busy Waiting 이기 때문
- 어쨋든
while
문을 통해 계속 CPU 와 메모리를 먹으면서 기다리기 때문에 - 불필요한 자원소모라고 할 수 있기 때문
- Spin lock 이라는 용어도 알아두라
- 어쨋든
Hardware approach (Atomic solution)
- 값을 읽는 작업과 쓰는 작업을 하나의 instruction 에서 처리할 수 있다면 동시성 문제가 좀 쉽게 해결될 수 있다
- 간단하게 생각해서 아래와 같은 코드로 두 프로세스가 돌아간다고 할 때
// global variable: boolean is_lock = false;
do {
while (is_lock);
is_lock = true;
critical_section();
is_lock = false;
remainder_section();
} while (1);
-
두번째 줄에서
is_lock
값을 확인해서false
가 나와 세번째 줄을 수행하려 할 때- Context switch 가 일어나 다른 프로세스가
is_lock
값을 바꾸고 Critical section 으로 들어간다면 - 다시 돌아왔을 때
is_lock
을 확인하지 않고 Critical section 으로 들어가기 때문에 두 프로세스가 모두 Critical section 에 진입하게 된다
- Context switch 가 일어나 다른 프로세스가
-
하지만 값을 읽는것과 쓰는 작업을 한번에 해주는 instruction 가 있다면 위와 같은 상황은 해결이 된다
- 아래의 코드에서
test_and_set()
함수는 변수의 값을 읽는 것과 값을true
로 바꾸는 작업을 한번에 한다고 가정하면
(사진 사라짐)
- 즉,
test_and_set()
함수가 변수의 값을 읽고false
라면true
로 바꾸고true
여도true
로 바꾸는 작업을 한다면
- 아래의 코드에서
// global variable: boolean is_lock = false;
do {
while (test_and_set(is_lock));
critical_section();
is_lock = false;
remainder_section();
} while (1);
- 그럼 2번째 줄을 수행한 다음 Context switch 가 일어나도
is_lock
값이 이미 바뀌어있기 때문에 다른 프로세스는 Critical section 으로 드가지 못한다
Semapore
- Semapore 은 동시성 처리를 위한 추상 자료형이다
- 즉, Semapore 는 Property 와 Method 만 정의되고 구현방식은 정의되지 않는다
- Semapore 의 Property 는 다음의 특징을 가져야 한다
- Integer: 셀 수 있는 정수값을 가진다
- Semapore 의 정수값은 자원에 접근할 수 있는 프로세스의 개수를 나타낸다
- 즉, 0보다 클 경우에는 해당 프로세스가 자원에 접근할 수 있다는 것을 나타내고 그렇지 않다면 대기해야 한다는 것을 의미한다
- 그리고 Method 는 다음과 같으며 해당 Method 들은 Atomic 하게 작동한다
- P: Semapore 의 값이 0보다 클 경우에는 1을 감소시키고 그렇지 않을 경우에는 대기한다.
- P 연산의 경우에는 Lock 을 거는 작업을 담당한다
- 1을 감소시키기 때문에 접근할 수 있는 프로세스의 개수를 하나 감소시켜 한 자리를 차지하는 셈인 거고
- 0 이하일 경우에는 대기하기 때문에 자리가 없을 경우 대기하는 것으로 해석할 수 있다
- V: Semapore 의 값을 1 증가시킨다
- V 연산의 경우에는 Lock 을 해제하는 작업을 담당한다
- 즉, 1을 증가시키기 때문에 Lock 을 풀고 한 자리를 내어놓는 것으로 해석할 수 있다
- P: Semapore 의 값이 0보다 클 경우에는 1을 감소시키고 그렇지 않을 경우에는 대기한다.
Implementation 1: Busy waiting (Spin lock)
- Go 로 대충 수도코드 적어보자고
- 일단
struct
type Semapore struct {
count int
}
- 그리고
method
두개
func (s *Semapore) P() {
for s.count <= 0 {}
s.count--
}
func (s *Semapore) V() {
s.count++
}
- 뭐 간단하죠?
- 근데 위에서 언급한것처럼 이 경우에는 반복문이 돌면서 기다리기 때문에 CPU와 메모리의 낭비이다
Implementation 2: Block wakeup (Sleep lock)
-
이번에는 대기할때 반복문을 도는게 아니고 아예 프로세스의 상태를 Blocked 상태로 바꿔버리는 방법이다
(사진 사라짐)
-
즉, 위 그림과 같이 IO 큐 등의 여러 큐들에 추가적으로 공유데이터에 접근하는 것을 기다리는 큐를 하나 더 둬서 대기시킨다
-
그래서 보통 아래처럼 구현한다
(사진 사라짐)
- PCB 큐를 둬서 하나의 세마포에 대기하도록 함
-
간단히 수도코드 적어보자고
-
세마포는 다음처럼 생각할 수 있음
type Semapore {
value int
wait []int
}
- 그리고 다음처럼 메소드들을 구현할 수 있을 것이다
func (s *Semapore) P() {
if s.value--; s.value < 0 {
s.wait = append(s.wait, os.Getpid())
os.Block() // Pseudo method `Block()`
}
}
func (s *Semapore) V() {
if s.value++; s.value <= 0 {
os.WakeUp(s.wait[0]) // Pseudo method `WakeUp(pid int)`
s.wait = s.wait[1:]
}
}
s.value++; s.value <= 0
의 이유: 일단 1을 더해줬는데도 0과 같거나 작다는 것은 1을 더해주기 전에는 0보다 작았었기 때문에 대기하던 프로세스가 있음을 의미
Busy-wait vs Block-wakeup
- 일반적으로는 Block-wakeup 방식이 더 좋기는 하지만
- Block-wakeup 방식의 Context switch 에 오버헤드가 존재하기 때문에 Critical section 이 아주 짧은 경우에는 Busy-wait 방식이 오히려 더 좋을 수 있다
Semapore 종류
- Countable semapore: 값이 2 이상이 될 수 있는 세마포
- 보통 자원의 수를 세는 용도로 사용됨
- Binary semapore(Mutex): 값이 0또는 1만이 되는 세마포
- 프로세스의 Mutual exclusion 을 위해 사용됨
Deadlock, Starvation
- Deadlock 은 둘 이상의 프로세스가 서로의 이벤트 종료를 기다리고 있는 상황이라고 할 수 있다
- 그니까 쉽게 말하면 내가 끝나려면 너가 끝나야되는데 너가 끝나려면 내가 끝나야되는 상황
- Starvation 은 둘 이상의 프로세스가 자기네들끼리만 우선권을 획득해서 일부 프로세스가 우선권을 영원히 획득할 수 없는 상태를 말한다
- 이 둘은 그 다음에 나오는 굶주린 소크라테스 보면 딱 이해됨
Bounded-Buffer Problem
(사진 사라짐)
- 이 문제는 다음과 같다:
- 공유 메모리에 있는 버퍼에는 값을 넣을 수 있는 칸이
n
개가 있다 → Bounded-buffer, 유한 버퍼 - 여러 Producer 가 값을 생산하여 버퍼의 한 칸에 채워넣는다
- 여러 Consumer 는 Producer 가 생산하여 버퍼에 채워넣은 값을 가져가 비운다
- 공유 메모리에 있는 버퍼에는 값을 넣을 수 있는 칸이
- 이 문제에는 다음과 같은 동시성 관리가 필요하다:
- Producer 혹은 Consumer 프로세스는 한번에 한놈만 공유 버퍼에 접근해야 한다
- 만일 그렇지 않은 경우에는 두 Producer 가 한번에 같은 칸에 접근해서 하나의 값이 덮어씌워지거나
- 두 Consumer 가 한번에 같은 칸에 접근해서 문제가 되거나 (뭐 같은 값을 두번 가져가거나 null 을 가져가거나 등등)
- Producer 는 비어있는 칸이 있어야 값을 쓸 수 있고 Consumer 는 채워져있는 칸이 있어야 값을 가져올 수 있다
- Producer 혹은 Consumer 프로세스는 한번에 한놈만 공유 버퍼에 접근해야 한다
- 그래서 이 문제에는 세개의 세마포가 사용된다
- 공유 버퍼에의 접근을 제어할 Mutex
- Producer 입장에서의 자원 관리
- 즉, 비어있는 칸이 Producer 입장에서의 자원이므로 이것을 관리할
empty_sem
이 하나 필요하다
- 즉, 비어있는 칸이 Producer 입장에서의 자원이므로 이것을 관리할
- Consumer 입장에서의 자원 관리
- 즉, 채워져있는 칸이 Consumer 입장에서의 자원이므로 이것을 관리할
full_sem
이 하나 필요하다
- 즉, 채워져있는 칸이 Consumer 입장에서의 자원이므로 이것을 관리할
- 따라서 Producer 와 Consumer 는 다음의 과정을 거쳐 작업을 수행한다
- Producer
- 비어있는 칸이 있는지 확인하고 없으면 기다림
- 공유데이터에 Lock 을 걺
- 데이터 입력
- Lock 을 풂
- 채워져 있는 칸의 개수를 1 증가시킴
- Consumer
- 채워져 있는 칸이 있는지 확인하고 없으면 기다림
- 공유데이터에 Lock 을 걺
- 데이터를 가져감
- Lock 을 풂
- 비어있는 칸의 개수를 1 증가시킴
- Producer
- 이를 바탕으로 수도코드를 적어보면 다음과 같다
- Producer
/**
* Shared memory
* var buf *bufio.ReadWriter
*
* Semapores
* var mutex semapore_t = 1
* var empty_sem semapore_t = n
* var full_sem semapore_t = 0
*/
func produce() []byte { /** DO SOMETHING */ }
func main() {
for {
value := produce()
P(empty_sem)
P(mutex)
buf.Write(value)
V(mutex)
V(full_sem)
}
}
- Consumer
/**
* Shared memory
* var buf *bufio.ReadWriter
*
* Semapores
* var mutex semapore_t = 1
* var empty_sem semapore_t = n
* var full_sem semapore_t = 0
*/
func consume(value []byte) { /** DO SOMETHING */ }
func main() {
for {
P(full_sem)
P(mutex)
var value []byte
buf.Read(value)
V(mutex)
V(empty_sem)
consume(value)
}
}
- 세마포 값은 다음과 같은 이유이다
mutex
의 경우에는 상보배제해야되므로 값이 1이고empty_sem
의 경우에는 처음에는 모두 비어있으니까 값이 n 이고full_sem
의 경우에는 처음에는 채워져있는게 하나도 없으니까 값이 0이다
- 그리고 과정을 차근차근 보면
- 일단 Producer 는
empty_sem
을 하나 먹고 값을 쓰되 - 릴리즈 과정에서
empty_sem
을 릴리즈하는게 아니고full_sem
을 릴리즈해서 1을 증가시킨다 - 그럼 Consumer 는
full_sem
을 먹고싶은데 일단은full_sem
이 0이니까 기다리다가 - Producer 가
full_sem
을 1 증가시키면 그걸 낼름 먹어서 값을 가져온다 - 그리고 이번에는
full_sem
을 릴리즈하는게 아니고empty_sem
을 릴리즈해서 1을 증가시키는 방식
- 일단 Producer 는
Readers-Writers Problem
- 이 문제는 DB 에서의 동시성 문제에 대한 간략한 예시이다:
- DB 에서 값을 읽는 것은 여러개가 접근해도 된다
- DB 에 값을 쓰는 것은 한놈만 접근해야 된다
- 이 문제에서는 다음의 세마포를 사용해 동시성을 관리할 수 있다:
- DB 에 배타적으로 값을 write 하기 위한 세마포
- Reader 의 개수를 세어서 Reader 가 있는 경우에는 Writer 가 접근하지 못하도록 해야 하는데 이때 Reader 들의 개수를 세기 위한 공유 변수에의 세마포
- 따라서 다음과 같이 수도코드를 작성할 수 있다
- Writer
/**
* Shared memory
* var db *bufio.ReadWriter
* var readCount int = 0
*
* Semapores
* var db_sem semapore_t = 1
* var rc_sem semapore_t = 1
*/
func getValue() []byte { /** DO SOMETHING */ }
func main() {
for {
value := getValue()
P(db_sem)
db.Write(value)
V(db_sem)
}
}
- Reader
/**
* Shared memory
* var db *bufio.ReadWriter
* var rc int = 0
*
* Semapores
* var db_sem semapore_t = 1
* var rc_sem semapore_t = 1
*/
func useValue(value []byte) { /** DO SOMETHING */ }
func main() {
for {
P(rc_sem)
if rc++; rc == 1 {
P(db_sem)
}
V(rc_sem)
var value []byte
db.Read(value)
P(rc_sem)
if rc--; rc == 0 {
V(db_sem)
}
V(rc_sem)
useValue(value)
}
}
- 차근차근 보면
db_sem
하고rc_sem
은 어차피 상호배제를 위한거니까 값이 1이고- Writer 의 경우에는
db_sem
을 잠그는 것 밖에 할게 없다 - 하지만 Reader 의 경우에는
rc
를 건들기 위해 아래위로rc_sem
을 이용하여 한번에 한놈만 접근할 수 있게 하고 - 첫 Reader 의 경우에만
db_sem
을 잠그고 마지막 Reader 만db_sem
을 풀어 이것에 대한 상호배제를 하게 한다
- Writer 의 경우에는
- 하지만 위의 코드는 Starvation 이 일어날 수 있다
- 왜냐면 Reader 가 다 빠져나간 경우에만
db_sem
이 풀리기 때문에 Reader 가 계속 들어오면 Writer 가 들어올 수 없기 때문
- 왜냐면 Reader 가 다 빠져나간 경우에만
Dining-Philosophers Problem
(사진 사라짐)
- 이건 배부른 돼지보다는 나은 배고픈 소크라테스의 고분분투기를 다룬 문제다
- 일단 상황은
- 소크라테스들이 자리에 앉아 생각을 하다가
- 배고프면 자신의 양쪽에 있는 젓가락을 둘 다 잡아 식사를 하고
- 이후에 다시 내려놓고 생각을 하는 고달픈 인생이다
- 이 상황을 타개할 수 있는 가장 간단한 해결법은 다음과 같다
- 왼쪽에 Lock 을 걸고 오른쪽에 Lock 을 걸어서 식사를 하고 차례대로 Lock 을 푸는 것
- 하지만 이것은 다음과 같은 문제가 생긴다
- Deadlock: 만약 모든 소크라테스가 왼쪽의 젓가락을 잡으면 아무도 식사하지 못한다
- Starvation: 만약 자신의 양옆에 있는애들이 번갈아서 식사하면 나는 굶어 죽어 배부른 돼지보다도 못하게 된다
- 이것을 해결할 수 있는 방법은 대표적으로
- 한번에 4명의 소크라테스만 앉게 한다
- 옆의 소크라테스를 유심히 보다가 젓가락을 모두 잡을 수 있을 때에만 식사를 한다
- 비대칭 → 짝수번째 소크라테스는 오른쪽부터, 홀수번째 소크라테스는 왼쪽부터
Monitor
- 동시성문제는 실행시에 무조건 발생하는 것이 아니라 코드를 잘못 작성했을 때 특정 조건이 맞을때에만 발생하기 때문에
- 세마포를 잘못 사용했을 때에 이것을 감지해내기 어렵다
- 따라서 개발자 입장에서 실수를 줄일 수 있는 더 상위 추상화가 여러 프로그래밍 언어들에서 지원되는데 이것이 Monitor 이다
(사진 사라짐)
-
그래서 위처럼 구성됨
-
모니터에서는 공유 데이터와 그것에 접근할 수 있는 유일한 방법인 메소드를 묶어 하나의 class 로 구현하게끔한다
-
구현한 뒤에는 Monitor 에는 한번에 하나의 프로세스만이 접근할 수 있도록 알아서 제어되기 때문에 Mutex 의 사용이 불필요하다
- 즉, 명시적으로 공유데이터를 잠그거나 푸는 로직을 작성하지 않아도 된다는 소리임
- 모니터에 접근한 프로세스가 종료되거나
- 뒤에 나오는 Condition 에 의해 Block 되는 등의 방식으로 모니터 사용이 끝나면 다른 프로세스가 모니터에 들어와서 사용하게 된다
-
그리고 모니터에는 Condition 기능도 제공되는데 이것은 Countable semapore 를 대체하는 기능이다
- Binary semapore(Mutex) 의 경우에는 모니터에서 알아서 해주니까 별다른 로직이 필요 없었지만
- 상호배제가 아닌 자원 개수 관리를 위한 Countable semapore 를 위해서 Condition 이 제공된다는 것이다
-
Condition 은 다음과 같은 두가지 기능을 가진다
Condition.wait()
: 얘는 현재의 프로세스를 Block 시키고 해당 Condition의 큐에 추가한다Condition.signal()
: 얘는 해당 Condition의 큐에서 프로세스 하나를 꺼내 Ready 로 바꾼다- 자고 있는 프로세스가 없을 경우에는 아무 작업도 하지 않는 로직도
signal()
에 내부적으로 구현되어 있다
- 자고 있는 프로세스가 없을 경우에는 아무 작업도 하지 않는 로직도
- 즉, 하나의 Condition 변수는 하나의 줄을 의미하고 두가지 기능으로 프로세스를 줄세우거나 줄에서 꺼내는 작업을 할 수 있는 것이라 생각하면 된다
- 다만 세마포의 P와 V는 자원의 개수를 값으로 가지고 필요한 자원이 있는지 없는지는 내부적으로 확인하는 대신
- Condition 을 사용할 때에는 필요한 자원이 있는지 없는지에 대한 로직은 개발자가 알아서 작성하고 재우거나 깨우는 것만 Condition 변수를 이용한다는 차이점이 있다
-
즉, Condition 을 통해 자원이 존재하지 않을 때 프로세스를 재우고 자원이 생기면 깨우는 로직을 손쉽게 구현할 수 있다
(사진 사라짐)
- 그래서 위에서 살펴본 Bounded-Buffer 문제를 Monitor 를 이용해 살펴보면 위처럼 됨
- Bounded-Buffer 가 Property 로 드가있고
- 여기에 접근할 수 있는 produce 와 consume 이 Method 로 드가있으며
- Monitor 자체에서 Mutex 가 지원되므로 Mutex 에 관련한 로직은 삭제되었고
- 자원 개수 관리에 대한 부분만 Condition 으로 대체된 것을 확인할 수 있다
- 그리고 Condition 으로
full
과empty
두개의 줄을 생성하고 - 조건에 따라 적절하게 프로세스를 해당 줄에서 대기하게 하거나 줄에서 꺼내는 등의 작업을 하게 됨
- 그리고 Condition 으로
Monitor vs Semapore
- 모니터와 세마포는 다음과 같은 방식으로 (거의) 1:1 변환된다
- 모니터 하나당 Mutex 를 위한 세마포를 선언한다
- 모니터의 Condition 하나당 Countable semapore 를 선언한다
- 모니터 메소드의 로직 중 자원 체크 & wait 혹은 signal 부분을 P 혹은 V 로 대체한다