이화여자대학교 컴퓨터공학과 반효경 교수님의 "운영체제 (KOCW)" 강의를 필기한 내용입니다.

다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.

문서를 이전하는 과정에서, 이미지 데이터가 유실되어 문서상에 보이지 않습니다.

Concurrency, Race Condition

(사진 사라짐)

  • 일반적으로는 위 그림처럼 데이터를 저장하는 곳하고 연산하는 곳하고는 분리되어 있으며
  • 연산하는 곳에서 데이터를 읽어들여 연산한 다음 저장하는 방식으로 작동되는데

(사진 사라짐)

  • 위 그림처럼 동일한 데이터에 여러 연산이 접근하게 되면 문제가 생길 수 있다
  • 이렇게 여러 연산이 하나의 데이터에 동시에 접근하는 문제를 Concurrency Problem, 동시성 문제 라 부르고
  • 동시성 문제가 발생하게 되는 상황을 연산간 경쟁한다는 의미로 Race Condition 이라고 부르더라
  • 이를 해결하기 위해서는 데이터의 접근 순서를 제어하는 로직이 필요하고 이런걸 Process Synchronization (프로세스 동기화) 라고 한다.

Common Race Condition Situations

  1. 커널 데이터
    • 일반 프로세스의 경우에는 자신만의 메모리 공간이 있기 때문에 동시성 문제 잘 발생하지 않지만

    • 커널의 경우에는 여러 프로세스가 Syscall 등을 이용해 공유할 수 있고

      (사진 사라짐)

      • 위 처럼 프로세스가 Syscall 을 해 커널모드에서 실행되다가 타임아웃이 난 후에 다른 프로세스로 넘어갔다가 여기서도 Syscall 을 걸어 커널 데이터를 변경하는 경우에 동시성 문제가 생길 수 있다
      • 이때는 커널모드일때는 CPU 를 Preempt 하지 못하게 하고 커널모드가 끝나야 빼앗을 수 있게 함으로써 해결할 수 있다
    • 커널모드에서 작업을 하다가 인터럽트가 걸리면 하던걸 멈추고 또 다른 커널 작업인 인터럽트 핸들링을 하게 되므로 이런 경우에도 문제가 생긴다

      (사진 사라짐)

      • 위 그림이 그 예시인데
      • 이러한 경우는 커널 모드 실행중일때는 인터럽트가 걸리지 않게 하는 방식으로 해결할 수 있다
  2. 공유 메모리, 쓰레드
    • 일반 프로세스에서 동시성 문제가 발생하는 경우 중 제일 흔한거는
    • 프로세스 간 공유 메모리를 할당받았거나
    • 멀티쓰레드 프로그래밍을 할 때이다
      • 멀티쓰레드의 경우에는 쓰레드 간 메모리가 공유되기 때문에 동시성 문제가 생길 수 있다

Handling Concurrency

Critical Section

(사진 사라짐)

  • 코드 상에서 공유 데이터 공간에 접근하는 부분을 Critical Section 이라고 한다.
  • 그리고 Entry / Exit Section 에서 Critical Section 에 들어가는 프로세스들을 Lock 을 거는것처럼 관리하게 된다.
  • 별로 중요한건 아니지만 공유데이터에 접근하지 않는 부분을 Remainder Section 이라고 한다

충족해야 할 조건들

  1. Mutual Exclusion: 상호 배제 → 하나의 프로세스가 Critical section 에 들어가 있으면 다른 프로세스는 들어가서는 안된다
  2. Progress: 현재 Critical section 에 들어가있는 프로세스가 없다면 Critical section 에 들어가고자 하는 프로세스는 거기 에 들어갈 수 있어야 한다.
  3. 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이 될 때까지 기다리기 때문에
    • 두놈이 같이 들어가는 상황은 막을 수 있음
  • 하지만 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 1Algorithm 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 에 진입하게 된다
  • 하지만 값을 읽는것과 쓰는 작업을 한번에 해주는 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 을 풀고 한 자리를 내어놓는 것으로 해석할 수 있다

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

(사진 사라짐)

  • 이 문제는 다음과 같다:
    1. 공유 메모리에 있는 버퍼에는 값을 넣을 수 있는 칸이 n 개가 있다 → Bounded-buffer, 유한 버퍼
    2. 여러 Producer 가 값을 생산하여 버퍼의 한 칸에 채워넣는다
    3. 여러 Consumer 는 Producer 가 생산하여 버퍼에 채워넣은 값을 가져가 비운다
  • 이 문제에는 다음과 같은 동시성 관리가 필요하다:
    1. Producer 혹은 Consumer 프로세스는 한번에 한놈만 공유 버퍼에 접근해야 한다
      • 만일 그렇지 않은 경우에는 두 Producer 가 한번에 같은 칸에 접근해서 하나의 값이 덮어씌워지거나
      • 두 Consumer 가 한번에 같은 칸에 접근해서 문제가 되거나 (뭐 같은 값을 두번 가져가거나 null 을 가져가거나 등등)
    2. Producer 는 비어있는 칸이 있어야 값을 쓸 수 있고 Consumer 는 채워져있는 칸이 있어야 값을 가져올 수 있다
  • 그래서 이 문제에는 세개의 세마포가 사용된다
    1. 공유 버퍼에의 접근을 제어할 Mutex
    2. Producer 입장에서의 자원 관리
      • 즉, 비어있는 칸이 Producer 입장에서의 자원이므로 이것을 관리할 empty_sem 이 하나 필요하다
    3. Consumer 입장에서의 자원 관리
      • 즉, 채워져있는 칸이 Consumer 입장에서의 자원이므로 이것을 관리할 full_sem 이 하나 필요하다
  • 따라서 Producer 와 Consumer 는 다음의 과정을 거쳐 작업을 수행한다
    • Producer
      1. 비어있는 칸이 있는지 확인하고 없으면 기다림
      2. 공유데이터에 Lock 을 걺
      3. 데이터 입력
      4. Lock 을 풂
      5. 채워져 있는 칸의 개수를 1 증가시킴
    • Consumer
      1. 채워져 있는 칸이 있는지 확인하고 없으면 기다림
      2. 공유데이터에 Lock 을 걺
      3. 데이터를 가져감
      4. Lock 을 풂
      5. 비어있는 칸의 개수를 1 증가시킴
  • 이를 바탕으로 수도코드를 적어보면 다음과 같다
  • 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을 증가시키는 방식

Readers-Writers Problem

  • 이 문제는 DB 에서의 동시성 문제에 대한 간략한 예시이다:
    • DB 에서 값을 읽는 것은 여러개가 접근해도 된다
    • DB 에 값을 쓰는 것은 한놈만 접근해야 된다
  • 이 문제에서는 다음의 세마포를 사용해 동시성을 관리할 수 있다:
    1. DB 에 배타적으로 값을 write 하기 위한 세마포
    2. 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 을 풀어 이것에 대한 상호배제를 하게 한다
  • 하지만 위의 코드는 Starvation 이 일어날 수 있다
    • 왜냐면 Reader 가 다 빠져나간 경우에만 db_sem 이 풀리기 때문에 Reader 가 계속 들어오면 Writer 가 들어올 수 없기 때문

Dining-Philosophers Problem

(사진 사라짐)

  • 이건 배부른 돼지보다는 나은 배고픈 소크라테스의 고분분투기를 다룬 문제다
  • 일단 상황은
    • 소크라테스들이 자리에 앉아 생각을 하다가
    • 배고프면 자신의 양쪽에 있는 젓가락을 둘 다 잡아 식사를 하고
    • 이후에 다시 내려놓고 생각을 하는 고달픈 인생이다
  • 이 상황을 타개할 수 있는 가장 간단한 해결법은 다음과 같다
    • 왼쪽에 Lock 을 걸고 오른쪽에 Lock 을 걸어서 식사를 하고 차례대로 Lock 을 푸는 것
  • 하지만 이것은 다음과 같은 문제가 생긴다
    • Deadlock: 만약 모든 소크라테스가 왼쪽의 젓가락을 잡으면 아무도 식사하지 못한다
    • Starvation: 만약 자신의 양옆에 있는애들이 번갈아서 식사하면 나는 굶어 죽어 배부른 돼지보다도 못하게 된다
  • 이것을 해결할 수 있는 방법은 대표적으로
    1. 한번에 4명의 소크라테스만 앉게 한다
    2. 옆의 소크라테스를 유심히 보다가 젓가락을 모두 잡을 수 있을 때에만 식사를 한다
    3. 비대칭 → 짝수번째 소크라테스는 오른쪽부터, 홀수번째 소크라테스는 왼쪽부터

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 으로 fullempty 두개의 줄을 생성하고
      • 조건에 따라 적절하게 프로세스를 해당 줄에서 대기하게 하거나 줄에서 꺼내는 등의 작업을 하게 됨

Monitor vs Semapore

  • 모니터와 세마포는 다음과 같은 방식으로 (거의) 1:1 변환된다
    • 모니터 하나당 Mutex 를 위한 세마포를 선언한다
    • 모니터의 Condition 하나당 Countable semapore 를 선언한다
    • 모니터 메소드의 로직 중 자원 체크 & wait 혹은 signal 부분을 P 혹은 V 로 대체한다