맨위로가기

모니터 (동기화)

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

모니터는 공유 데이터에 대한 동기화를 제공하는 프로그래밍 개념이다. 프로세스가 공유 데이터에 접근하기 전에 모니터에 들어가야 하며, 모니터 내에서는 공유 데이터에 대한 접근을 제어하고 경쟁 상태를 방지한다. 모니터는 프로시저, 뮤텍스 락, 변수, 불변 조건 등으로 구성되며, 조건 변수를 통해 스레드 간의 대기 및 신호 처리를 관리한다. 블로킹과 논블로킹 방식이 있으며, 자바, C# 등 일부 언어에서는 객체를 모니터처럼 사용할 수 있다. 생산자-소비자 문제와 같은 동시성 문제를 해결하는 데 활용되며, 브린치 한센과 호어에 의해 개발되었다.

더 읽어볼만한 페이지

  • 동시성 제어 - 세마포어
    세마포어는 데이크스트라가 고안한 정수 변수로, P/V 연산을 통해 자원 접근을 제어하고 동기화 문제를 해결하며, 계수 세마포어와 이진 세마포어로 나뉘어 멀티스레드 환경에서 자원 관리 및 스레드 동기화에 기여한다.
  • 동시성 제어 - 원자성
    원자성은 분자를 구성하는 원자 수로, 분자는 원자성에 따라 단원자, 이원자, 삼원자 분자 등으로 나뉘며, 금속이나 탄소는 원자성이 2로 간주되고 단원자 분자의 원자성은 분자량을 원자량으로 나누어 계산한다.
모니터 (동기화)
일반 정보
컴퓨터 과학에서의 모니터
모니터의 예시
유형동기화 구조
설계 패러다임객체 지향 프로그래밍
디자인 소개퍼 브린치=한센
토니 호어
소개 연도1973년, 1974년
세부 사항
관련상호 배제
세마포어
뮤텍스
병행성
스레드
프로세스
사용처자바
C 샤프
파이썬
스칼라

루비
오브젝티브-C
스위프트
구현
구성 요소공유 변수
프로시저
조건 변수
목적상호 배제 보장 및 경쟁 조건 방지
작동 방식한 번에 하나의 스레드만 모니터 내에서 활성화되도록 보장
조건 변수 사용스레드가 특정 조건이 충족될 때까지 대기하고, 다른 스레드가 해당 조건을 충족시키면 신호를 보내 대기 중인 스레드를 깨움
장점
주요 이점상호 배제를 쉽게 달성하고, 경쟁 조건 및 교착 상태를 방지하는 데 도움
높은 수준의 동기화프로그래머가 더 쉽게 이해하고 사용할 수 있는 추상화 제공
코드 가독성 향상동기화 로직을 캡슐화하여 코드의 가독성과 유지 보수성을 향상
단점
잠재적인 오버헤드모니터 내에서의 상호 배제 및 스레드 전환으로 인해 성능 오버헤드가 발생할 수 있음
유연성 제한특정 동기화 패턴을 지원하기 어려울 수 있음
교착 상태 가능성부적절하게 사용될 경우 교착 상태를 유발할 수 있음
역사적 맥락
최초 개념퍼 브린치=한센과 토니 호어가 1970년대 초반에 제안
구현 언어Concurrent Pascal과 같은 초기 병행 프로그래밍 언어에 구현
대안
주요 대안세마포어
뮤텍스
상호 배제
메시지 전달

2. 동작

어떤 공유 데이터에 대해 모니터를 지정해 놓으면, 프로세스는 그 데이터에 접근하기 위해 반드시 모니터에 들어가야 한다. 즉, 모니터 내부에 들어간 프로세스에게만 공유 데이터에 대한 접근 권한이 부여된다. 프로세스가 모니터에 들어가려고 할 때 다른 프로세스가 이미 모니터 내부에 있다면, 해당 프로세스는 입장 큐에서 대기해야 한다.

모니터는 공유 자원을 조작하는 프로시저군 (모니터 프로시저, 모니터 함수), 뮤텍스 락, 자원과 연결된 변수, 경쟁 상태를 방지하기 위해 가정되는 모니터 불변 조건으로 구성된다.

모니터 프로시저는 무언가를 하기 전에 을 걸고, 처리가 완료되거나 어떤 조건을 기다릴 때까지 걸어둔다. 각 프로시저가 락을 해제할 때 불변 조건이 참임을 보장한다면, 경쟁 상태가 되는 자원의 상태는 각 태스크에서는 보이지 않게 된다.

간단한 예로, 계좌의 거래를 위한 모니터를 생각해 볼 수 있다.

2. 1. 상호 배제 (Mutual Exclusion)

스레드가 스레드 안전 객체의 메서드를 실행하는 동안, 해당 객체의 뮤텍스(잠금)을 보유함으로써 해당 객체를 '점유'한다. 스레드 안전 객체는 '각 시점에서, 최대 하나의 스레드만이 객체를 점유할 수 있도록' 구현된다. 처음에 잠금 해제된 락은 각 공용 메서드의 시작 부분에서 잠기고, 각 공용 메서드에서 반환될 때 잠금 해제된다.[1]

메서드 중 하나를 호출하면, 스레드는 다른 스레드가 스레드 안전 객체의 메서드를 실행하고 있지 않을 때까지 대기해야 한다. 이 상호 배제가 없으면 두 개의 스레드가 이유 없이 돈을 잃거나 얻을 수 있다. 예를 들어, 두 개의 스레드가 계정에서 1000USD을 인출할 때 둘 다 true를 반환할 수 있지만 잔액은 1000USD만 감소한다. 먼저, 두 스레드 모두 현재 잔액을 가져와서 1000USD보다 크다는 것을 확인하고 1000USD을 뺀다. 그런 다음 두 스레드 모두 잔액을 저장하고 반환한다.[1]

3. 조건 변수 (Condition Variables)

조건 변수는 특정 조건이 참이 될 때까지 스레드가 대기할 수 있도록 하는 동기화 메커니즘이다. 조건 변수는 뮤텍스와 관련된 스레드들의 큐를 가지며, 스레드는 조건 변수에서 대기하면서 모니터 점유를 일시적으로 해제한다.

많은 애플리케이션에서 상호 배제만으로는 충분하지 않다. 스레드는 특정 조건 가 참이 될 때까지 기다려야 할 수 있다. 바쁜 대기 루프는 상호 배제가 다른 스레드가 조건을 참으로 만들기 위해 모니터에 들어가는 것을 막기 때문에 작동하지 않는다.

해결책은 조건 변수를 사용하는 것이다. 개념적으로 조건 변수는 뮤텍스와 관련된 스레드 큐이며, 스레드는 특정 조건이 참이 될 때까지 대기할 수 있다. 각 조건 변수 는 어설션 와 관련이 있다. 스레드가 조건 변수를 기다리는 동안에는 모니터를 점유하지 않으므로, 다른 스레드가 모니터에 들어가 상태를 변경할 수 있다.

조건 변수와 관련된 세 가지 주요 연산은 다음과 같다.


  • `'''wait''' c, m`: 조건 변수 `c`와 모니터와 관련된 뮤텍스(잠금) `m`을 사용한다. 어설션 가 참이 될 때까지 기다린 후 진행해야 하는 스레드에 의해 호출된다. 스레드가 대기하는 동안에는 모니터를 점유하지 않는다.
  • `'''signal''' c` (또는 `'''notify''' c`): 어설션 가 참임을 나타내기 위해 스레드에 의해 호출된다.
  • `'''broadcast''' c` (또는 `'''notifyAll''' c`): `c`의 대기 큐에 있는 모든 스레드를 깨운다.


여러 조건 변수를 동일한 뮤텍스와 연관시킬 수 있지만, 그 반대는 불가능하다. 생산자-소비자 문제에서 큐는 고유한 뮤텍스 객체 `''m''`으로 보호되어야 한다. "생산자" 스레드는 잠금 `''m''`과 큐가 비어 있지 않을 때까지 블록하는 조건 변수 c_{full}을 사용하여 모니터를 기다리고, "소비자" 스레드는 동일한 뮤텍스 `''m''`을 사용하지만 큐가 비어 있지 않을 때까지 블록하는 다른 조건 변수 c_{empty}를 사용하는 다른 모니터를 기다린다.

모니터는 다음으로 구성된다.

  • 공유 자원을 조작하는 프로시저
  • 뮤텍스 락
  • 자원과 연결된 변수
  • 경쟁 상태를 방지하기 위해 가정되는 모니터 불변 조건


모니터 프로시저는 무언가를 하기 전에 을 걸고, 처리가 완료되거나 어떤 조건을 기다릴 때까지 걸어둔다.

비지 웨이트 상태가 되는 것을 막기 위해, 프로세스는 서로에게 이벤트를 알릴 수단을 가지고 있어야 한다. 모니터는 이를 조건 변수로 구현한다.

3. 1. 조건 변수의 연산

조건 변수는 모니터 내에서 스레드 간 동기화를 위해 사용되는 핵심 메커니즘이다. 조건 변수는 특정 조건이 참이 될 때까지 스레드를 대기시키고, 다른 스레드에 의해 조건이 변경되었을 때 대기 중인 스레드에게 알림을 보내는 기능을 제공한다.

조건 변수에는 세 가지 주요 연산이 존재한다.

연산설명
`wait(c, m)`
`signal(c)` (또는 `notify(c)`)
`broadcast(c)` (또는 `notifyAll(c)`)


`wait` 연산의 원자성:`wait` 연산 내에서 뮤텍스 해제, 대기 큐로 이동, 스레드 일시 중단(1a, 1b, 1c 단계)은 원자적으로, 즉, 다른 스레드에 의한 간섭 없이 한 번에 수행되어야 한다. 이는 경쟁 조건, 특히 "놓친 깨우기(missed wakeup)" 문제를 방지하기 위해 필수적이다. "놓친 깨우기"는 스레드가 대기 큐에 들어가기 직전에 다른 스레드가 신호를 보내어, 해당 스레드가 영원히 대기 상태에 빠지는 현상이다.
`signal` vs. `broadcast`:`signal`은 대기 중인 스레드 중 하나만 깨우는 반면, `broadcast`는 모든 대기 중인 스레드를 깨운다. `broadcast`는 동일한 조건 변수에 대해 여러 개의 조건(술어)이 연결된 경우에 유용하다. 예를 들어, 생산자-소비자 문제에서 큐가 비어있지 않기를 기다리는 소비자와 큐가 가득 차지 않기를 기다리는 생산자가 동일한 조건 변수를 사용할 수 있다. 이 경우, `signal`은 잘못된 유형의 스레드를 깨울 수 있으므로 `broadcast`를 사용해야 한다.
모니터 사용 패턴:모니터를 사용할 때는 일반적으로 다음과 같은 패턴을 따르며, 조건 변수 `cv`를 기다리기 전에 조건 `p`를 확인하고, `wait` 호출 후에도 조건을 다시 확인하는 것이 중요하다.

```cpp

acquire(m); // 뮤텍스 m 획득

while (!p) { // 조건 p가 참이 아닐 동안

wait(m, cv); // 뮤텍스 m을 해제하고 조건 변수 cv에서 대기

}

// ... 임계 영역 ...

signal(cv2); // 또는 broadcast(cv2); // 다른 조건 변수 cv2에 신호

release(m); // 뮤텍스 m 해제

```

이는 `wait`에서 복귀한 후에도 다른 스레드에 의해 조건이 다시 거짓이 될 수 있기 때문이다(가짜 깨우기, spurious wakeup).

3. 2. 블로킹 조건 변수 (Blocking Condition Variables)

C. A. R. 호어와 페르 브린치 한센의 초기 제안은 '''블로킹 조건 변수'''에 대한 것이었다. 블로킹 조건 변수를 사용하면 신호를 보내는 스레드는 (최소한) 신호를 받은 스레드가 반환하거나 다시 조건 변수에서 대기하여 모니터의 점유를 포기할 때까지 모니터 외부에서 대기해야 한다. 이러한 모니터를 ''호어 스타일'' 모니터 또는 ''신호 및 긴급 대기'' 모니터라고 한다.[4][5]

두 개의 조건 변수 ab를 가진 호어 스타일 모니터. Buhr ''et al.'' 이후


각 모니터 객체와 관련된 두 개의 스레드 큐가 있다고 가정한다.

  • `e`는 입장 큐이다.
  • `s`는 신호를 보낸 스레드의 큐이다.


또한 각 조건 변수 에 대해 다음 큐가 있다고 가정한다.

  • `.q`는 조건 변수 에서 대기하는 스레드의 큐이다.


모든 큐는 일반적으로 공정하도록 보장되며, 일부 구현에서는 선입선출이 보장될 수 있다.

각 연산의 구현은 다음과 같다. (각 연산이 서로 상호 배타적으로 실행된다고 가정한다. 따라서 재시작된 스레드는 연산이 완료될 때까지 실행을 시작하지 않는다.)

모니터 입력:

: 메서드 입력

: '''if''' 모니터가 잠겨 있으면

:: 이 스레드를 e에 추가

:: 이 스레드를 블록

: '''else'''

:: 모니터를 잠금

모니터 나가기:

: 스케줄링

: '''return''' from the method

'''대기''' :

: 이 스레드를 .q에 추가

: 스케줄링

: 이 스레드를 블록

'''신호''' :

: '''if''' .q에서 대기하는 스레드가 있는 경우

:: .q에서 그러한 스레드 t를 선택하고 제거한다.

:: (t를 "신호를 받은 스레드"라고 한다)

:: 이 스레드를 s에 추가

:: t를 다시 시작한다.

:: (t가 다음에 모니터를 점유한다)

:: 이 스레드를 블록

스케줄링:

: '''if''' s에 스레드가 있는 경우

:: s에서 하나의 스레드를 선택하고 제거한 후 다시 시작한다.

:: (이 스레드가 다음에 모니터를 점유한다)

: '''else if''' e에 스레드가 있는 경우

:: e에서 하나의 스레드를 선택하고 제거한 후 다시 시작한다.

:: (이 스레드가 다음에 모니터를 점유한다)

: '''else'''

:: 모니터 잠금 해제

:: (모니터가 점유되지 않음)

`schedule` 루틴은 모니터를 점유할 다음 스레드를 선택하거나, 후보 스레드가 없는 경우 모니터의 잠금을 해제한다.

결과적인 시그널링 규율은 시그널러가 대기해야 하지만 입장 큐의 스레드보다 우선 순위가 부여되므로 ''"신호 및 긴급 대기"''라고 한다. 대안은 `s` 큐가 없고 시그널러가 대신 `e` 큐에서 대기하는 ''"신호 및 대기"''이다.

일부 구현은 시그널링과 프로시저에서 반환을 결합하는 '''신호 및 반환''' 연산을 제공한다.

'''신호''' '''및 반환''':

: '''if''' .q에서 대기하는 스레드가 있는 경우

:: .q에서 그러한 스레드 t를 선택하고 제거한다.

:: (t를 "신호를 받은 스레드"라고 한다)

:: t를 다시 시작한다.

:: (t가 다음에 모니터를 점유한다)

: '''else'''

:: 스케줄링

: '''return''' from the method

어떤 경우든 ("신호 및 긴급 대기" 또는 "신호 및 대기") 조건 변수에 신호가 전달되고 조건 변수에서 대기하는 스레드가 하나 이상 있으면 신호를 보낸 스레드가 신호를 받은 스레드에게 점유를 원활하게 넘겨주므로 다른 스레드가 그 사이에 점유를 얻을 수 없다. 각 '''signal''' 연산이 시작될 때 가 true이면 각 '''wait''' 연산이 끝날 때 true가 된다. 이는 다음과 같은 계약으로 요약된다. 이러한 계약에서 는 모니터의 불변식이다.

모니터 입력:

: '''사후 조건'''

모니터 나가기:

: '''사전 조건'''

'''대기''' :

: '''사전 조건'''

: 모니터의 상태를 '''수정'''한다.

: '''사후 조건''' '''and'''

'''신호''' :

: '''사전 조건''' '''and'''

: 모니터의 상태를 '''수정'''한다.

: '''사후 조건'''

'''신호''' '''및 반환''':

: '''사전 조건''' '''and'''

이러한 계약에서 와 는 큐의 내용이나 길이에 의존하지 않는다고 가정한다.

(조건 변수가 큐에서 대기하는 스레드 수를 쿼리할 수 있는 경우, 보다 정교한 계약을 제공할 수 있다. 예를 들어, 불변식을 설정하지 않고 점유를 전달할 수 있는 유용한 계약 쌍은 다음과 같다.

'''대기''' :

: '''사전 조건'''

: 모니터의 상태를 '''수정'''한다.

: '''사후 조건'''

'''신호'''

: '''사전 조건''' ('''not''' empty() '''and''' ) '''or''' (empty() '''and''' )

: 모니터의 상태를 '''수정'''한다.

: '''사후 조건'''

여기서 어서션은 전적으로 프로그래머에게 달려 있다는 점에 유의하는 것이 중요하다. 즉, 프로그래머는 그것이 무엇인지에 대해 일관성을 유지해야 한다.

이 섹션의 마지막으로 블로킹 모니터를 사용하여 경계가 있고 스레드 안전한 스택을 구현하는 스레드 안전 클래스의 예를 보여준다.

'''모니터 클래스''' ''SharedStack'' {

: '''private const''' capacity := 10

: '''private''' ''int''[capacity] A

: '''private''' ''int'' size := 0

: '''불변식''' 0 <= size '''and''' size <= capacity

: '''private''' ''BlockingCondition'' theStackIsNotEmpty /* '''associated with''' 0 < size '''and''' size <= capacity */

: '''private''' ''BlockingCondition'' theStackIsNotFull /* '''associated with''' 0 <= size '''and''' size < capacity */

: '''public method''' push(''int'' value)

: {

:: '''if''' size = capacity '''then''' '''wait''' theStackIsNotFull

:: '''assert''' 0 <= size '''and''' size < capacity

:: A[size] := value ; size := size + 1

:: '''assert''' 0 < size '''and''' size <= capacity

:: '''signal''' theStackIsNotEmpty '''and return'''

: }

: '''public method''' ''int'' pop()

: {

:: '''if''' size = 0 '''then''' '''wait''' theStackIsNotEmpty

:: '''assert''' 0 < size '''and''' size <= capacity

:: size := size - 1 ;

:: '''assert''' 0 <= size '''and''' size < capacity

:: '''signal''' theStackIsNotFull '''and return''' A[size]

: }

}

이 예에서 스레드 안전 스택은 내부적으로 뮤텍스를 제공하며, 이는 이전 생산자/소비자 예제에서와 같이 동일한 동시 데이터에 대한 다른 조건을 확인하는 두 개의 조건 변수가 공유하며, 모니터의 이러한 세부 정보가 여기에 있는 것처럼 추상화되지 않은 경우뿐이다. 이 예에서 "대기" 연산이 호출되면 "대기" 연산이 "모니터 클래스"의 통합된 부분인 경우처럼 스레드 안전 스택의 뮤텍스가 어떻게든 제공되어야 한다. 이러한 종류의 추상화된 기능을 제외하고 "raw" 모니터를 사용할 때는 항상 뮤텍스와 조건 변수를 포함해야 하며, 각 조건 변수에 대해 고유한 뮤텍스가 있어야 한다.

3. 3. 논블로킹 조건 변수 (Nonblocking Condition Variables)

'''논블로킹 조건 변수'''는 ''"Mesa 스타일"'' 조건 변수 또는 ''"신호 및 계속"'' 조건 변수라고도 불린다. 신호를 보내는 스레드가 모니터 점유를 잃지 않는다는 특징이 있다. 대신 신호를 받은 스레드는 `e` 큐로 이동한다.

두 개의 조건 변수 `a`와 `b`를 가진 Mesa 스타일 모니터


논블로킹 조건 변수에서는 '''신호''' 연산을 '''알림'''이라고 부르며, 조건 변수에서 대기 중인 모든 스레드를 `e` 큐로 이동시키는 '''모두 알림''' 연산도 제공하는 것이 일반적이다.

각 연산의 의미는 다음과 같다. (각 연산은 다른 연산과 상호 배타적으로 실행된다고 가정한다. 따라서 다시 시작된 스레드는 연산이 완료될 때까지 실행을 시작하지 않는다.)

  • 모니터 진입:
  • 메서드 진입 시 모니터가 잠겨 있으면 이 스레드를 `e`에 추가하고 차단한다.
  • 모니터가 잠겨 있지 않으면 모니터를 잠근다.
  • 모니터 나가기:
  • 스케줄을 호출하고 메서드에서 반환(`return`)한다.
  • '''대기''' (wait) :
  • 이 스레드를 .q에 추가한다.
  • 스케줄을 호출하고 이 스레드를 차단한다.
  • '''알림''' (notify) :
  • .q에 대기 중인 스레드가 있으면 그 중 하나를 선택하여 제거하고 `e`로 이동한다. (이 스레드를 "알림을 받은 스레드"라고 한다.)
  • '''모두 알림''' (notifyAll) :
  • .q에 대기 중인 모든 스레드를 `e`로 이동한다.
  • 스케줄:
  • `e`에 스레드가 있으면 그 중 하나를 선택하여 제거하고 다시 시작한다.
  • `e`에 스레드가 없으면 모니터 잠금을 해제한다.


이러한 체계의 변형으로 알림을 받은 스레드를 `e`보다 우선 순위가 높은 `w`라는 큐로 이동시킬 수 있다.

각 조건 변수 에 어서션(assertion)을 연결하여 `'''대기''' `에서 반환될 때 가 참이 되도록 보장할 수 있다. 그러나 '''알림''' 스레드가 점유를 포기하는 시점부터 알림을 받은 스레드가 모니터에 다시 진입할 때까지 가 보존되도록 해야 한다. 이 시간 동안 다른 스레드에 의한 활동이 있을 수 있으므로, 일반적으로 는 단순히 ''true''이다.

이러한 이유로, 일반적으로 다음과 같은 루프에 각 '''대기''' 연산을 묶어야 한다.

'''while''' '''not''' ( ) '''do'''

'''wait''' c

여기서 는 보다 강력한 조건이다. `'''알림''' ` 및 `'''모두 알림''' ` 연산은 일부 대기 스레드에 대해 가 참일 수 있다는 "힌트"로 취급된다. 이러한 루프의 첫 번째 반복 이후의 각 반복은 손실된 알림을 나타내므로, 논블로킹 모니터에서는 너무 많은 알림이 손실되지 않도록 주의해야 한다.

"힌트"의 예로, 계좌에 충분한 자금이 있을 때까지 출금 스레드가 대기하는 은행 계정 모니터를 들 수 있다.



'''모니터 클래스''' ''Account'' {

'''private''' ''int'' balance := 0

'''invariant''' balance >= 0

'''private''' ''NonblockingCondition'' balanceMayBeBigEnough

'''public method''' withdraw(''int'' amount)

'''precondition''' amount >= 0

{

'''while''' balance < amount '''do''' '''wait''' balanceMayBeBigEnough

'''assert''' balance >= amount

balance := balance - amount

}

'''public method''' deposit(''int'' amount)

'''precondition''' amount >= 0

{

balance := balance + amount

'''notify all''' balanceMayBeBigEnough

}

}



이 예에서 대기 중인 조건은 출금할 금액의 함수이므로 입금 스레드가 그러한 조건을 참으로 만들었는지 ''알 수'' 없다. 따라서 각 대기 스레드가 모니터에 들어가 (한 번에 하나씩) 해당 어서션이 참인지 확인하는 것이 타당하다.

3. 4. 암시적 조건 변수 (Implicit Condition Variables)

자바와 C# 같은 언어에서는 각 객체가 모니터 역할을 할 수 있다. 상호 배제를 위해 메서드는 `'''synchronized'''` 키워드로 표시하며, 코드 블록도 `'''synchronized'''`로 표시할 수 있다.[1]

이 방식에서 각 모니터(객체)는 진입 큐 외에 단일 대기 큐를 가진다. 모든 대기와 `'''notify'''`, `'''notifyAll'''` 작업은 이 단일 큐에서 이루어진다.[2]

모니터는 다음 요소로 구성된다.

  • 공유 자원을 다루는 프로시저 (모니터 프로시저, 모니터 함수)
  • 뮤텍스 락
  • 자원과 연결된 변수
  • 경쟁 상태 방지를 위한 모니터 불변 조건


모니터 프로시저는 작업을 시작하기 전에 을 걸고, 처리가 끝나거나 특정 조건을 기다릴 때까지 유지한다. 각 프로시저가 락을 해제할 때 불변 조건이 참이면, 경쟁 상태는 발생하지 않는다.

다음은 계좌 거래를 위한 모니터의 간단한 예이다.

```

monitor account {

int balance := 0

function withdraw(int amount) {

if amount < 0 then error "Amount may not be negative"

else if balance < amount then error "Insufficient funds"

else balance := balance - amount

}

function deposit(int amount) {

if amount < 0 then error "Amount may not be negative"

else balance := balance + amount

}

}

```

이 예에서 모니터 불변 조건은 "새로운 조작을 할 때 이전에 수행된 모든 조작이 balance에 반영되어야 한다"는 것이다. 이는 코드에 직접 명시되지 않지만, 주석으로 작성된다. Eiffel 같은 언어는 불변 조건 검사를 도입하고, 락은 컴파일러가 추가하여 안전성을 높인다.

3. 5. 암시적 신호 (Implicit Signaling)

신호 연산을 생략하고, 스레드가 모니터를 떠날 때마다 대기 중인 모든 스레드의 어서션(Assertion)을 평가한다. 이러한 시스템에서는 조건 변수가 필요하지 않지만, 어서션은 명시적으로 코딩되어야 한다. 대기 계약은 다음과 같다.

  • '''wait''':
  • '''사전 조건'''
  • '''수정''' 모니터의 상태
  • '''사후 조건''' '''그리고'''

3. 6. 생산자-소비자 문제 (Producer-Consumer Problem)

조건 변수를 사용한 고전적인 동시성 문제 해결 방식이다. 생산자-소비자 문제는 최대 크기를 가진 또는 링 버퍼를 사용하여, 생산자 스레드는 큐에 작업을 추가하고 소비자 스레드는 큐에서 작업을 가져가는 방식으로 동작한다. 이때 큐가 가득 차면 생산자는 소비자가 작업을 제거할 때까지 대기하고, 큐가 비면 소비자는 생산자가 작업을 추가할 때까지 대기해야 한다.

큐에 접근하는 코드는 임계 구역으로, 원자적으로 접근이 이루어져야 하며, 상호 배타적 제어를 통해 동기화되어야 한다. 그렇지 않으면 경쟁 조건이 발생할 수 있다.

초기의 단순한 접근 방식은 '''바쁜 대기'''를 사용하는 것이었다. 그러나 이 방식은 CPU 리소스를 낭비하는 문제가 있다. 생산자가 작업을 추가할 수 없거나 소비자가 작업을 가져갈 수 없을 때 불필요하게 CPU를 점유하게 된다.

이러한 문제점을 해결하기 위해 조건 변수를 사용한다. 조건 변수는 특정 조건이 충족될 때까지 스레드를 대기시키고, 조건이 충족되면 스레드에 신호를 보내 깨우는 방식으로 동작한다.

다음은 조건 변수를 사용하여 생산자-소비자 문제를 해결하는 C++ 코드 예시이다.

```cpp

global volatile RingBuffer queue; // 작업의 스레드 안전하지 않은 링 버퍼입니다.

global Lock queueLock; // 작업의 링 버퍼에 대한 뮤텍스입니다. (스핀 잠금이 아닙니다.)

global CV queueEmptyCV; // 큐가 비어 있지 않게 되기를 기다리는 소비자 스레드에 대한 조건 변수입니다.

// 연관된 잠금은 "queueLock"입니다.

global CV queueFullCV; // 큐가 가득 차지 않게 되기를 기다리는 생산자 스레드에 대한 조건 변수입니다.

// 연관된 잠금도 "queueLock"입니다.

// 각 생산자 스레드의 동작을 나타내는 메서드:

public method producer() {

while (true) {

// 생산자는 추가할 새로운 작업을 만듭니다.

task myTask = ...;

// 초기 조건 확인을 위해 "queueLock"을 획득합니다.

queueLock.acquire();

// 큐가 가득 차지 않았는지 확인하는 중요 섹션입니다.

while (queue.isFull()) {

// "queueLock"을 해제하고, 이 스레드를 "queueFullCV"에 대기열에 넣고 이 스레드를 잠재웁니다.

wait(queueLock, queueFullCV);

// 이 스레드가 깨어나면 다음 조건 확인을 위해 "queueLock"을 다시 획득합니다.

}

// 작업이 큐에 추가되는 중요 섹션입니다(우리는 "queueLock"을 가지고 있습니다).

queue.enqueue(myTask);

// 큐가 비어 있지 않다는 것이 보장되었으므로 큐를 기다리는 하나 또는 모든 소비자 스레드를 깨워 소비자 스레드가 작업을 가져가도록 합니다.

signal(queueEmptyCV); // 또는: broadcast(queueEmptyCV);

// 중요 섹션의 끝입니다.

// 다음 작업을 추가하기 위해 다시 필요할 때까지 "queueLock"을 해제합니다.

queueLock.release();

}

}

// 각 소비자 스레드의 동작을 나타내는 메서드:

public method consumer() {

while (true) {

// 초기 조건 확인을 위해 "queueLock"을 획득합니다.

queueLock.acquire();

// 큐가 비어 있지 않은지 확인하는 중요 섹션입니다.

while (queue.isEmpty()) {

// "queueLock"을 해제하고, 이 스레드를 "queueEmptyCV"에 대기열에 넣고 이 스레드를 잠재웁니다.

wait(queueLock, queueEmptyCV);

// 이 스레드가 깨어나면 다음 조건 확인을 위해 "queueLock"을 다시 획득합니다.

}

// 큐에서 작업을 가져가는 중요 섹션입니다(우리는 "queueLock"을 가지고 있습니다).

myTask = queue.dequeue();

// 큐가 가득 차지 않다는 것이 보장되었으므로 큐를 기다리는 하나 또는 모든 생산자 스레드를 깨워 생산자 스레드가 작업을 추가하도록 합니다.

signal(queueFullCV); // 또는: broadcast(queueFullCV);

// 중요 섹션의 끝입니다.

// 다음 작업을 가져오기 위해 다시 필요할 때까지 "queueLock"을 해제합니다.

queueLock.release();

// 작업을 처리합니다.

doStuff(myTask);

}

}

```

위 코드에서는 `queueLock`이라는 뮤텍스와 `queueEmptyCV`, `queueFullCV`라는 두 개의 조건 변수를 사용한다. 생산자는 큐가 가득 차면 `queueFullCV`에서 대기하고, 소비자는 큐가 비면 `queueEmptyCV`에서 대기한다. `signal` 함수는 대기 중인 스레드 중 하나를 깨우고, `broadcast` 함수는 대기 중인 모든 스레드를 깨운다.

이 코드는 하나의 조건 변수(`queueFullOrEmptyCV`)와 `broadcast`만을 사용하는 방식으로 변경할 수도 있다. 이 경우, 생산자와 소비자 모두 동일한 조건 변수에서 대기하고, `signal` 대신 `broadcast`를 사용하여 모든 대기 중인 스레드를 깨워야 한다.

조건 변수를 사용하면 불필요한 자원 낭비를 줄이고 효율적으로 동시성 문제를 해결할 수 있다.

4. 세마포어와 모니터

세마포어는 동기화 함수의 제약 조건을 고려해야 하는 반면, 모니터는 프로시저를 호출하여 간단히 해결할 수 있다. 스레드가 스레드 안전 객체의 메서드를 실행하는 동안, 해당 객체의 뮤텍스(잠금)을 보유함으로써 해당 객체를 '점유'한다고 말한다.[1] 스레드 안전 객체는 '각 시점에서, 최대 하나의 스레드만이 객체를 점유할 수 있도록' 구현된다.[1] 처음에 잠금 해제된 락은 각 공용 메서드의 시작 부분에서 잠기고, 각 공용 메서드에서 반환될 때 잠금 해제된다.[1]

메서드 중 하나를 호출하면, 스레드는 다른 스레드가 스레드 안전 객체의 메서드를 실행하고 있지 않을 때까지 대기해야 한다.[2] 이 상호 배제가 없으면 두 개의 스레드가 이유 없이 돈을 잃거나 얻을 수 있다.[2] 예를 들어, 두 개의 스레드가 계정에서 1000USD을 인출할 때 둘 다 true를 반환할 수 있지만 잔액은 1000USD만 감소한다.[2] 먼저, 두 스레드 모두 현재 잔액을 가져와서 1000USD보다 크다는 것을 확인하고 1000USD을 뺀다.[2] 그런 다음 두 스레드 모두 잔액을 저장하고 반환한다.[2]

모니터는 다음으로 구성된다.[3]


  • 공유 자원을 조작하는 프로시저군 (모니터 프로시저, 모니터 함수)[4]
  • 뮤텍스 락[4]
  • 자원과 연결된 변수[4]
  • 경쟁 상태를 방지하기 위해 가정되는 모니터 불변 조건[4]


모니터 프로시저는 무언가를 하기 전에 을 걸고, 처리가 완료되거나 어떤 조건을 기다릴 때까지 걸어둔다 (조건에 대해서는 후술).[5] 각 프로시저가 락을 해제할 때 불변 조건이 참임을 보장한다면, 경쟁 상태가 되는 자원의 상태는 각 태스크에서는 보이지 않게 된다.[5]

단순한 예로, 계좌의 거래를 위한 모니터를 생각해보자.[6]

```

monitor account {

int balance := 0

function withdraw(int amount) {

if amount < 0 then error "Amount may not be negative"

else if balance < amount then error "Insufficient funds"

else balance := balance - amount

}

function deposit(int amount) {

if amount < 0 then error "Amount may not be negative"

else balance := balance + amount

}

}

```

이 경우의 모니터 불변 조건은 간단히 말해 "새로운 조작을 할 때 이전에 수행된 모든 조작이 balance에 반영되어야 한다"는 것이다.[8] 이것은 코드 자체에는 적혀 있지 않지만, 일반적으로 주석에 기재될 것이다.[8] 예를 들어, Eiffel과 같은 언어는 불변 조건의 검사를 도입하고, 락은 컴파일러에 의해 추가된다.[8] 이것은 프로그래머가 락과 언락을 일일이 써야 하는 언어보다 안전하고 신뢰성이 높다.[8]

5. 역사

브린치 한센과 앤서니 호어(C. A. R. Hoare)는 1970년대 초, 자신들과 에츠허르 데이크스트라의 이전 아이디어를 바탕으로 모니터 개념을 개발했다.[6] 브린치 한센은 시뮬라클래스 개념을 채택하여 최초의 모니터 표기법을 발표했으며,[1] 큐잉 메커니즘을 발명했다.[7] 호어는 프로세스 재개 규칙을 개선했다.[2] 브린치 한센은 Concurrent Pascal에서 모니터의 첫 번째 구현을 만들었다.[6] 호어는 이를 세마포어와 동등함을 입증했다.

모니터(및 Concurrent Pascal)는 곧 Solo 운영 체제에서 프로세스 동기화를 구조화하는 데 사용되었다.[8][9]

6. 모니터를 지원하는 프로그래밍 언어

다음은 모니터를 지원하는 프로그래밍 언어 목록이다.

프로그래밍 언어설명
에이다Ada 95부터 보호 객체로 지원한다.
C#.NET 프레임워크를 사용하는 다른 언어도 지원한다.
Concurrent Euclid
Concurrent Pascal
D
델파이델파이 2009 이상부터 TObject.Monitor를 통해 지원한다.
자바wait 및 notify 메서드를 통해 지원한다.
Go[10][11]
Mesa
Modula-3
파이썬[https://docs.python.org/library/threading.html#condition-objects threading.Condition] 객체를 통해 지원한다.
루비
Squeak Smalltalk
튜링튜링+, Object-Oriented Turing도 지원한다.
μC++
Visual Prolog



Pthreads와 같이 모니터를 기본적으로 지원하지 않는 언어에서도 여러 라이브러리를 통해 모니터를 구축할 수 있다. 라이브러리 호출이 사용될 때는 상호 배타적으로 실행되는 코드의 시작과 끝을 프로그래머가 명시적으로 표시해야 한다.

참조

[1] 서적 Operating System Principles https://archive.org/[...] Prentice Hall
[2] 논문 Monitors: an operating system structuring concept 1974-10
[3] 논문 The programming language Concurrent Pascal https://authors.libr[...] 1975-06
[4] 간행물 Signaling in monitors http://dl.acm.org/ci[...] IEEE Computer Society Press
[5] 논문 Monitor classification 1995-03
[6] 간행물 Monitors and concurrent Pascal: a personal history Association for Computing Machinery
[7] 논문 Structured multiprogramming (Invited Paper) 1972-07
[8] 논문 The Solo operating system: a Concurrent Pascal program http://brinch-hansen[...] 1976-04
[9] 서적 The Architecture of Concurrent Programs Prentice Hall
[10] 웹사이트 sync - The Go Programming Language https://golang.org/p[...] 2021-06-17
[11] 웹사이트 What's the "sync.Cond" {{!}} dtyler.io https://dtyler.io/ar[...] 2021-06-17
[12] 문서 Monitor Class (System.Threading) | Microsoft Learn https://learn.micros[...]
[13] 문서 lock ステートメント - 共有リソースへのスレッド アクセスを同期します - C# | Microsoft Learn https://learn.micros[...]
[14] 문서 std::condition_variable - cppreference.com https://ja.cpprefere[...]
[15] 문서 条件変数の利用方法 - cpprefjp C++日本語リファレンス https://cpprefjp.git[...]
[16] 문서 System.SyncObjs.TConditionVariableMutex - RAD Studio API Documentation https://docwiki.emba[...]
[17] 문서 pthread_cond_timedwait, pthread_cond_wait - wait on a condition | The Open Group Base Specifications Issue 6 / IEEE Std 1003.1, 2004 Edition https://pubs.opengro[...]
[18] 문서 Synchronization - 1.84.0 / §Condition Variables https://www.boost.or[...]
[19] 문서 thread/include/boost/thread/win32/condition_variable.hpp at boost-1.84.0 · boostorg/thread · GitHub https://github.com/b[...]
[20] 문서 Condition Variables - Win32 apps | Microsoft Learn https://learn.micros[...]



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com