본문 바로가기

컴퓨터/운영체제

[운영체제] 동기화(Synchronization)

동기화는 컴퓨팅 시스템에서 여러 프로세스나 스레드가 자원에 대한 접근을 조정하고, 데이터의 일관성을 유지하기 위해 사용되는 기술이다.

 

같은 컴퓨터에서 서로 다른 프로세스가 통신을 하여 정보를 주고 받는 멀티프로세싱 환경에서 Synchronization이 발생하는데 이때는 같은 컴퓨터 내부임으로 Inter Process Communication (IPC) 이다.

 

IPC는 다른 프로세스 간에 데이터를 주고받는 통신 메커니즘으로 같은 시스템 내의 프로세스 간 통신에 사용되며, 메시지 전달, 공유 메모리, 파이프라인 등 다양한 방법으로 구현될 수 있다.

Synchronization 이유

다른 두 컴퓨터가 매우 먼 거리로 떨어져 있는 경우는 Network로 연결되어 프로세스 정보를 주고 받는다.

이러한 상황에서 동기화를 하는 이유는 다음과 같다.

  1. 데이터 일관성 유지
    서로 다른 컴퓨터에서 동일한 데이터에 접근하거나 수정할 때, 동기화가 제대로 이루어지지 않으면 데이터의 일관성이 깨질 수 있다.
    예를 들어, 하나의 데이터베이스를 두 컴퓨터가 동시에 업데이트하려 한다면 충돌이 발생할 수 있다.

  2. 경쟁 상태(Race Conditions) 방지
    Critical Section (임계 영역)은 여러 프로세스 또는 스레드가 동시에 접근하려고 할 때 데이터의 일관성을 유지하기 위해 동시 접근이 제한되어야 하는 코드의 일부분을 뜻한다.
    이 영역에서는 데이터, 파일, 입출력 장치등의 공유 자원이 처리 된다.
    그리고 프로세스 내부에서 실행되는 과정 중 임계 영역이 다른 프로세스의 개입으로 인해 방해받을 경우 경쟁 상태(Race Condition)가 발생한다.
    따라서 여러 컴퓨터가 동시에 같은 자원에 접근하려 할 때, 어느 하나의 작업이 완료되기 전에 다른 작업이 시작되어 예상치 못한 결과를 초래할 수 있다.

  3. 분산 시스템의 복잡성
    네트워크를 통한 프로세스 간 통신은 지연, 네트워크 장애, 메시지 순서 변경 등 다양한 문제에 직면할 수 있다.
    이런 상황에서 동기화는 시스템의 신뢰성을 보장하는 데 필수적이다.

  4. 분산 잠금 및 트랜잭션 관리
    분산 시스템에서는 잠금(Locking) 메커니즘과 트랜잭션 관리를 통해 여러 컴퓨터 간 데이터의 무결성을 유지해야 한다.

Synchronization 요구사항

주요 조건 (Primary Requirements)

  • Mutual Exclusion (상호 배제)
    임계 영역에 어느 한 시점에는 하나의 프로세스만이 접근할 수 있어야 한다.
    이는 여러 프로세스가 동시에 공유 자원을 사용할 때 발생할 수 있는 데이터의 불일치나 경쟁 상태를 방지하는 데 필수적이다.

  • Progress (진행)
    임계 영역이 비어있고 접근을 시도하는 프로세스가 있을 때, 그 중 하나가 결국 임계 영역에 접근할 수 있도록 보장해야 한다.
    즉, 임계 영역에 접근하려는 프로세스가 있다면, 그 접근이 결국에는 허용되어야 한다.

보조 조건 (Secondary Requirements):

  • Bounded Waiting (유한 대기)
    각 프로세스는 임계 영역에 접근하기 위해 무한정 기다리지 않아야 한다.
    즉, 모든 프로세스는 기아 현상이 생기지 않고 유한한 시간 안에 임계 영역에 접근할 기회를 받아야 한다.

  • Architectural Neutrality/Portability (구조적 중립성/이식성)
    동기화 메커니즘은 특정 하드웨어나 운영 체제에 의존하지 않아야 한다.
    즉, 다양한 시스템 및 플랫폼에서도 동일하게 작동할 수 있어야 한다.

Busy Waiting을 통한 프로세스 Synchronization 

이 방식은 프로세스가 임계 영역에 접근할 수 있는지 계속 확인하면서, CPU 시간을 소모하며 대기하는 방식이다.

1. Lock Variable

이는 특별한 하드웨어 지원 없이도 구현될 수 있으며, 프로그래밍 언어 수준에서 적용되는 방식이다.

Lock Variable은 일반적으로 두 가지 상태를 가지는데 Lock이 0인
상태는 임계 영역이 비어있으며, 어떤 프로세스도 임계 영역에 접근할 수 있다는 것을 의미한다.
반대로 Lock이 1인 상태는
임계 영역이 사용 중임을 나타내며, 다른 프로세스는 접근할 수 없다는 것을 의미한다.

동작 방식은 프로세스가 임계 영역에 접근하려고 할 때, 먼저 Lock Variable을 검사하여 만약 Lock이 0이면 프로세스는 Lock을 1로 변경하고 임계 영역에 진입한다.
그리고 작업을 끝났으면 나오면서 Lock을 다시 0으로 설정한다.
만약 Lock이 1이면, 프로세스는 Lock이 0이 될 때까지 계속 검사를 진행하며 대기한다.

 

- Spin Lock

만약 Lock이 존재하여 임계 영역에 접근할 수 없을 때 접근할 수 있을 때까지 프로세스나 스레드가 Busy Waiting 상태로 남아있는 것을 의미한다.

만약 진행이 막혀 Context Switching을 하게 된다면 CPU의 부담이 커지게 된다.

그리고 임계 영역의 대기 시간이 매우 짧은 경우 잠금을 기다리는 시간이 프로세스 컨텍스트 스위칭에 소요되는 시간보다 짧아 Spin Lock을 사용하면 성능이 향상된다.

다만, 임계 영역의 대기 시간이 길어질 경우 CPU 자원을 낭비할 수 있다.

 

int lock = 0; // 초기에는 잠금이 해제된 상태
void enter_critical_section() {
    while (lock != 0) { // 다른 프로세스가 임계 영역을 빠져나올 때까지 기다린다.
    }
    lock = 1; // 임계 영역에 진입하기 전에 잠금을 설정
}
void leave_critical_section() {
    lock = 0; // 임계 영역을 빠져나오면서 잠금을 해제
}

간단한 Lock Variable은 위와 같이 구현 된다.

 

이제 두 개의 프로세스가 동시에 enter_critical_section 함수를 호출하는 상황을 가정해볼 때 다음과 같은 문제점이 있다.

  1. 두 프로세스가 동시에 임계 영역에 진입 시도
    두 프로세스가 거의 동시에 enter_critical_section을 호출하게 되어서 한 프로세스가 Lock을 검사하고 임계 영역에 진입하기 전 Lock을 1로 설정하려고 할 때 다른 프로세스가 Lock을 검사하고 0으로 판단할 수 있다.
    이 경우 두 프로세스 모두
    while 루프를 통과하고 임계 영역에 진입하게 되어 Mutual Exclusion을 위반하게 된다.

  2. 잠금을 해제하지 않고 임계 영역을 빠져나오는 경우
    프로세스가 leave_critical_section 함수를 호출하지 않고 임계 영역에서 종료가 된다면 Lock이 계속 1로 남아 있게된다.
    이 경우 lock 변수는 계속 1로 남게 되어, 다른 프로세스가 임계 영역에 접근할 수 없게 된다.

2. Test and Set(TAS)

TAS은 동시에 여러 프로세스 또는 스레드가 공유 자원에 접근하는 것을 제어하는 데 사용된다.

TAS명령의 핵심은 변수의 값을 읽고, 그 변수를 설정하는 두 단계의 작업을 원자적으로 수행하여 명령어가 실행되는 도중에 끼어들 수 없게 하는 것이다.

 

int test_and_set(int *lock) {
    int old = *lock;
    *lock = 1;
    return old;
}
int lock = 0; // 공유 자원에 대한 잠금 변수
void enter_critical_section() {
    while (test_and_set(&lock) == 1) { // 다른 프로세스가 임계 영역을 빠져나올 때까지 기다린다.
    } // 이 시점에서, lock은 1로 설정되었고 임계 영역에 접근할 수 있다.
}
void leave_critical_section() {
    lock = 0; // 임계 영역을 빠져나오면서 잠금을 해제
}

 

TAS은 Lock 변수를 사용하여 임계 영역에 대한 접근을 관리한다.

프로세스 또는 스레드가 임계 영역에 접근하려고 할 때 Lock 변수의 값을 읽고, 즉시 그 값을 1로 설정한다.

만약 Lock 변수의 원래 값이 0이었다면 이 프로세스는 임계 영역에 진입할 수 있다.

만약 Lock 변수의 원래 값이 1이었다면 이는 다른 프로세스가 이미 임계 영역에 접근하고 있음을 의미하므로 해당 프로세스는 대기한다.

 

상호 배제 (Mutual Exclusion)

TAS 명령은 원자적으로 lock을 테스트하고 설정하기 때문에 한 시점에 하나의 프로세스만이 임계 영역에 접근할 수 있도록 보장한다.

따라서 이는 동시에 두 개 이상의 프로세스가 임계 영역에 진입하는 것을 방지한다.

 

진행 (Progress)

TAS는 임계 영역이 사용 가능할 때 대기 중인 프로세스 중 하나가 임계 영역에 진입할 수 있도록 하여 대기 중인 프로세스들은 TAS 명령을 통해 lock을 1로 설정하고 임계 영역에 진입할 수 있다.

따라서 이는 어느 프로세스도 무한히 대기하지 않고, 임계 영역이 비어 있을 때 적절한 프로세스가 이를 활용할 수 있도록 한다.

 

Bounded Waiting

임계 영역이 해제되었을 때 여러 프로세스가 동시에 TAS 명령을 실행할 수 있다.

이 경우, 어떤 프로세스가 임계 영역에 진입할 것인지는 운영 체제의 스케줄링 정책과 시스템의 타이밍에 따라 달라질 수 있는데 특정 프로세스가 임계 영역에 접근하는 데 필요한 시간에 대한 명확한 상한이 없기 때문에 특정 프로세스가 계속해서 임계 영역에 진입하는 데 실패할 수 있어 Bounded Waiting을 위반한다.

즉, 어떤 프로세스는 다른 프로세스가 임계 영역을 반복적으로 사용하는 동안 장시간 대기할 수 있다.

3. Disabling Interrupts

인터럽트를 일시적으로 비활성화함으로써 다른 프로세스가 임계 영역에 동시에 접근하는 것을 방지하는 방법이다.

결과적으로 임계 영역 내에서의 연산이 중단되지 않고 완료될 수 있도록 보장한다.

다만 이러한 동작은 인터럽트 기반의 I/O 처리와 같은 시스템의 반응성을 저하시킬 수 있고 잘못된 사용은 시스템의 안정성을 위협하고 Deadlock의 위험을 증가시킬 수 있다.

 

Mutual Exclusion

인터럽트를 비활성화함으로써 현재 실행 중인 코드가 완료될 때까지 다른 프로세스나 스레드가 실행되지 않도록 하여 상호 베제를 보장한다.

 

Progress

인터럽트가 비활성화되면 다른 프로세스가 임계 영역에 접근을 시도하여 임계 영역에 진입할 수 없게 된다.

그리고 프로세스가 작업을 완료하고 임계 영역을 떠날 때 인터럽트를 다시 활성화하여 다른 프로세스가 임계 영역에 진입할 수 있는 기회를 준다.

따라서 인터럽트가 활성화된 상태에서 다른 프로세스가 임계 영역에 접근을 시도하면 해당 프로세스는 임계 영역에 진입할 수 있으므로 Progress의 요구 사항이 충족이 된다.

 

Bounded Waiting

한 프로세스가 임계 영역에 오래 머무를 경우 다른 프로세스는 그 기간 동안 대기해야 하며 대기 시간에 상한이 없기 때문에 유한한 대기를 보장하지 않는다.

 

Architectural Neutrality/Portability

인터럽트 비활성화는 특정 하드웨어와 운영 체제에 의존적인 방법이다.

따라서 모든 시스템에서 이를 지원하지 않으며, 특히 사용자 레벨의 애플리케이션에서는 이 방법을 사용할 수 없다.

4. Turn Variable(Strict Alternation Approach)

소프트웨어 기반의 동기화 메커니즘으로 주로 두 프로세스 간의 상호 배제를 위해 사용된다.

 

이 방식에서 Turn은 어느 프로세스의 차례인지를 나타내는 글로벌 변수로, 0과 1의 두 가지 값만을 가진다.

예를 들어, Turn = 0이면 프로세스 P0가 임계 영역에 진입할 수 있음을 나타내고, Turn = 1이면 프로세스 P1의 차례임을 의미한다.

 

int turn; // 글로벌 변수로 선언
const int P0 = 0;
const int P1 = 1;
void enter_critical_section_P0() { // 프로세스 P0의 임계 영역 접근 함수
    while (turn != P0) { // P0의 차례가 될 때까지 기다린다.
    } // 이제 P0의 임계 영역 코드 실행
}
void leave_critical_section_P0() {
    turn = P1; // P1의 차례로 변경
}
void enter_critical_section_P1() { // 프로세스 P1의 임계 영역 접근 함수
    while (turn != P1) { // P1의 차례가 될 때까지 기다린다.
    } // 이제 P1의 임계 영역 코드 실행
}
void leave_critical_section_P1() {
    turn = P0; // P0의 차례로 변경
}

위는 Trun Variable을 사용하는 코드의 예시이다.

 

이 방식의 단점은 한 프로세스가 임계 영역에 진입하지 않을 경우, 다른 프로세스는 임계 영역에 접근할 수 없다는 것이다.

예를 들어, P0이 임계 영역에 진입하지 않는다면 turn은 계속 0으로 남아 있어 P1은 임계 영역에 접근할 수 없다.

이는 동기화의 Progress요구 사항을 만족시키지 못하지 못한다.

 

Mutual Exclusion

각 프로세스는 turn 변수의 값에 따라 임계 영역에 진입할 수 있으며, 이 변수는 한 번에 한 프로세스만 변경할 수 있다.

예를 들어, turn이 0이면 프로세스 P0만 임계 영역에 진입할 수 있고, turn이 1이면 P1만 진입할 수 있다.

이렇게 하여 두 프로세스가 동시에 임계 영역에 접근하는 것을 방지한다.

 

Progress

만약 프로세스 P0가 임계 영역에 진입할 필요가 없는 경우에도 turn 변수가 그 프로세스를 가리키고 있다면, 다른 프로세스P1는 임계 영역에 진입할 수 없다.

따라서, 한 프로세스의 비활성화가 다른 프로세스의 진행을 방해할 수 있으므로 Progress요구 사항을 위반한다.

 

Bounded Waiting

각 프로세스는 다른 프로세스가 임계 영역을 사용하고 난 후에 자신의 차례가 된다.

따라서, 한 프로세스가 임계 영역을 무한히 점유할 수 없으며, 각 프로세스는 임계 영역에 진입하기 위해 유한한 시간을 기다리게 된다.

 

Architectural Neutrality/Portability

소프트웨어 기반의 동기화 방법으로 특정 하드웨어나 시스템에 의존하지 않는다.

따라서 이 방식은 다양한 컴퓨팅 환경에서 구현할 수 있으며, 이식성이 높다.

 

5.  Interested Variable

소프트웨어 기반의 동기화 메커니즘으로 주로 두 개의 프로세스를 위해 설계되어 각 프로세스가 임계 영역에 진입하고자 하는 의사를 나타내는 두 개의 변수를 사용한다.

 

P0와 P1 두 개의 프로세스는 각각 interested[0]과 interested[1]라는 두 개의 변수를 사용한다.

초기 상태에서는 두 변수 모두 False로 설정되어 어떤 프로세스도 임계 영역에 진입하고자 하는 의사가 없음을 나타낸다.

어떤 프로세스가 임계 영역에 진입하고자 할 때, 해당 프로세스의 interested 변수를 True로 설정한다.

이때 만약 다른 프로세스의 interested 변수가 False이면, 현재 프로세스는 임계 영역에 진입할 수 있다.

하지만 두 프로세스 모두 interested를 True로 설정한 경우 먼저 interested를 설정한 프로세스가 우선적으로 임계 영역에 진입한다.

 

이를 바탕으로 짠 코드를 보면 다음과 같다.

 

bool interested[2] = {false, false}; // 각 프로세스의 흥미를 나타내는 배열
const int P0 = 0; // 프로세스 0을 나타내는 상수
const int P1 = 1; // 프로세스 1을 나타내는 상수
void enter_critical_section_P0() { // 프로세스 0의 임계 영역 접근 함수
    interested[P0] = true; // 임계 영역에 진입하고 싶다고 표시
    while (interested[P1]) {
        // 프로세스 1도 임계 영역에 진입하고자 하는지 확인
        // P1의 흥미가 사라질 때까지 기다린다
    }
    // 임계 영역 코드 실행
}
void leave_critical_section_P0() {
    interested[P0] = false; // 임계 영역을 떠나면서 흥미 제거
}
void enter_critical_section_P1() { // 프로세스 1의 임계 영역 접근 함수
    interested[P1] = true;
    while (interested[P0]) {
        // P0의 흥미가 사라질 때까지 기다린다
    }
    // 임계 영역 코드 실행
}
void leave_critical_section_P1() {
    interested[P1] = false;
}

 

Mutual Exclusion

각 프로세스가 임계 영역에 진입하기 전에 자신의 흥미를 true로 설정하고, 만약 다른 프로세스도 흥미를 true로 설정했다면 두 프로세스 중 하나의 프로세스만 임계 영역에 진입하게 된다.

이는 어느 한 순간에 한 프로세스만 임계 영역에 진입할 수 있도록 하여 상호 배제를 보장한다.

 

Progress

임계 영역이 비어 있고 두 프로세스 모두 진입을 원하지 않는 경우 임계 영역에 진입하려는 프로세스는 즉시 진입할 수 있다.

이 방식은 임계 영역이 비어 있을 때 적어도 하나의 프로세스가 진입할 수 있는 기회를 제공함으로 Progress를 만족한다.

 

Bounded Waiting

만약 한 프로세스가 계속해서 자신의 흥미를 true로 유지하고 임계 영역에 진입하지 않는다면 다른 프로세스는 무한히 대기하게 된다.

이는 유한 대기 조건을 위반하게 되어 모든 프로세스가 공정하게 임계 영역에 접근할 수 있는 기회를 갖는 것이 보장되지 않는다.

 

Architectural Neutrality/Portability

소프트웨어 기반의 동기화 방법으로 특정 하드웨어나 시스템에 의존하지 않는다.

따라서 이 방식은 다양한 컴퓨팅 환경에서 구현할 수 있으며, 이식성이 높다.

 

6. Peterson's Solution

두 개의 프로세스 간에 상호 배제를 보장하기 위해 설계된 소프트웨어 기반 동기화 메커니즘으로 Interested Variable과 Turn Variable을 모두 사용하여 Mutual Exclusion, Progress, Bounded Waiting, Architectural Neutrality/Portability를 모두 만족시킨다.

여기서

interested 배열은 각 프로세스가 임계 영역에 진입하고자 하는지 여부를 나타내고, turn 변수는 어느 프로세스의 차례인지를 나타낸다.

프로세스는 임계 영역에 진입하기 전에 자신의 interested를 true로 설정하고, turn을 자신의 번호로 설정한다.

다른 프로세스의 interested가 true이거나 turn이 다른 번호이면 다른 프로세스가 임계 영역을 사용 중이거나 사용할 차례임을 나타냄으로 자리가 빌 때까지 대기한다.

 

이를 바탕으로 간단히 구현하면 다음과 같다.

 

bool interested[2] = {false, false}; // 각 프로세스의 흥미를 나타내는 배열
int turn; // 현재 임계 영역에 접근할 차례를 나타내는 변수
const int P0 = 0;
const int P1 = 1;
void enter_critical_section_P0() { // 프로세스 0의 임계 영역 접근 함수
    interested[P0] = true; // 임계 영역에 진입하고자 함을 표시
    turn = P1; // 프로세스 1에게 임계 영역 접근 차례를 줌
    while (interested[P1] && turn == P1) { // 프로세스 1의 흥미가 있고, 차례가 P1일 때까지 기다림
    }
}
void leave_critical_section_P0() {
    interested[P0] = false; // 임계 영역을 떠나며 흥미 제거
}
void enter_critical_section_P1() { // 프로세스 1의 임계 영역 접근 함수
    interested[P1] = true;
    turn = P0;
    while (interested[P0] && turn == P0) { // 프로세스 0의 흥미가 있고, 차례가 P0일 때까지 기다림
    }
}
void leave_critical_section_P1() {
    interested[P1] = false;
}

 

 

Mutual Exclusion

각 프로세스는 임계 영역에 진입하기 전에 자신의 interestedtrue로 설정하고, turn 변수를 자신의 번호로 설정한다.

이 과정에서 한 프로세스만이 임계 영역에 진입할 수 있어 두 프로세스가 동시에 임계 영역에 접근하는 것을 방지한다.

 

Progress

임계 영역이 비어 있고 두 프로세스 중 하나가 진입을 원하는 경우 해당 프로세스는 진입할 수 있다.

이 방식은 임계 영역이 비어 있을 때 적어도 하나의 프로세스가 진입할 수 있는 기회를 제공함으로 Progress를 만족한다.

 

Bounded Waiting

만약 한 프로세스가 임계 영역에 진입하고자 할 때 다른 프로세스가 이미 임계 영역에 있거나 진입을 기다리고 있다면 interestedturn 변수를 통해 두 프로세스가 공정하게 임계 영역에 진입할 수 있도록 한다.

이때 turn 변수는 프로세스가 임계 영역을 떠날 때마다 바뀌므로, 어떤 프로세스도 무한히 대기하는 상황이 발생하지 않는다.

결국, 모든 프로세스는 유한시간 내에 임계 영역에 접근할 수 있는 차례를 갖게 된다.

 

Architectural Neutrality/Portability

소프트웨어 기반의 동기화 방법으로 특정 하드웨어나 시스템에 의존하지 않는다.

따라서 이 방식은 다양한 컴퓨팅 환경에서 구현할 수 있으며, 이식성이 높다.

Busy Waiting을 사용하지 않는 프로세스 Synchronization 

Busy Waiting은 프로세스가 임계 영역에 접근할 수 있을 때까지 끊임없이 상태를 체크하는 방식이다.
이는 CPU 자원을 지속적으로 소모하며, 특히 우선순위 역전(Priority Inversion) 문제를 일으킬 수 있다.

예를 들어, 우선순위가 낮은 프로세스가 임계 영역을 점유하고 있을 때 우선순위가 높은 프로세스가 대기해야 하는 상황이 발생한다.

 

- Sleep and Wake up

Busy Waiting이 없는 동기화 메커니즘에서는 프로세스가 임계 영역에 진입할 수 없을 때 sleep 상태로 들어간다.

이는 프로세스가 CPU 자원을 소모하지 않고 대기하게 하여, 시스템의 전체적인 효율성을 향상시킨다.

그 후 임계 영역이 사용 가능해지면 대기 중인 프로세스는 Wake Up하여 임계 영역에 진입한다. 

 

- Sleep and Wake up의 문제점

 

Producer-Consumer problem
멀티프로세싱 환경에서 흔히 발생하는 동기화 문제 중 하나로 데이터를 생성하는 Producer와 데이터를 소비하는 Consumer 사이의 동기화를 필요로 하는 문제이다.
이들은 공유 버퍼를 사용하여 데이터를 주고받는데 버퍼의 상태에 따라 Producer와 Consumer의 동작을 적절히 조절해야한다.


Producer 동작
Producer는 데이터 item을 생성하고 생성된 항목을 공유 버퍼에 저장한다.
만약 버퍼가 가득 차서 더 이상 공간이 없다면 Producer는 sleep 상태로 전환되어 버퍼에 공간이 생길 때까지 기다린다.
Consumer가 데이터 항목을 소비하고 버퍼에 공간이 생기면 sleep 상태에 있는 Producer가 깨어나 데이터 항목을 버퍼에 추가한다.

Consumer 동작
Consumer는 공유 버퍼에서 데이터 항목을 소비하는데 만약 버퍼가 비어 있다면 Consumer는 sleep 상태로 전환되어 대기한다.
그러다 Producer가 새로운 데이터 항목을 버퍼에 추가하면, sleep 상태에 있는 Consumer는 깨어나 항목을 소비한다.

 

#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0; // 버퍼에 저장된 항목 수
void producer() { // Producer 함수
    int item;
    while (true) {
        item = produce_item(); // 항목 생성
        if (count == BUFFER_SIZE) {
            wait(); // 버퍼가 가득 차면 대기 (데드락 위험)
        }
        buffer[count] = item; // 버퍼에 항목 추가
        count++;
    }
}
void consumer() { // Consumer 함수
    int item;
    while (true) {
        if (count == 0) {
            wait(); // 버퍼가 비어 있으면 대기 (데드락 위험)
        }
        item = buffer[count - 1]; // 버퍼에서 항목 가져오기
        count--;
        consume_item(item); // 항목 소비
    }
}

 

Producer와 Consumer 모두 버퍼의 상태에 따라 대기 상태로 전환될 수 있다.

만약 Producer가 버퍼가 가득 찼을 때 대기 상태로 전환되거나, Consumer가 버퍼가 비어 있을 때 대기 상태로 전환된다면 두 프로세스 모두 영원히 대기 상태에 머무르게 되는 deadlock이 발생한다.

 

Semaphore

동시성 프로그래밍에서 중요한 동기화 도구로 공유 자원에 대한 접근을 조절하는 데 사용된다.

세마포어는 binary semaphore와 counting semaphore로 크게 두 종류가 있다.

 

 - counting semaphore

카운팅 세마포어는 동시에 여러 프로세스가 임계 영역에 접근할 수 있도록 허용한다.

세마포어의 값은 공유 자원에 동시에 접근할 수 있는 프로세스의 최대 수를 나타낸다.

이때 세마포어 값이 양수이면 그 값은 임계 영역에 들어갈 수 있는 남은 프로세스 수를 나타내고, 음수인 경우 값의 절대값이 현재 대기 중인 프로세스 수를 나타낸다.

그리고 값이 0이면 현재 모든 임계 영역이 사용 중이므로 추가 프로세스는 대기해야 함을 의미한다.

 

상호 배제

카운팅 세마포어는 상호 배제를 보장하지 않아 여러 프로세스가 동시에 임계 영역에 접근할 수 있다.

 

Progress

카운팅 세마포어는 Progress를 보장하여 임계 영역에 자리가 있으면 대기 중인 프로세스 중 하나가 진입할 수 있다.

 

Bounded Waiting

카운팅 세마포어는 유한 대기를 보장하여 대기 중인 프로세스는 유한 시간 내에 임계 영역에 진입할 수 있다.

 

Architectural Neutrality/Portability

세마포어는 운영 체제가 지원해야 하는 동기화 도구로, 모든 시스템 또는 환경에서 지원되는 것은 아니다.

이는 특정 운영 체제에 종속적일 수 있으며, 구조적 중립성이나 이식성을 완전히 만족시키지는 못한다.

 

 - Binary semaphore

Binary Semaphore는 두 가지 상태 즉, 잠금(locked, 0) 상태와 해제(unlocked, 1) 상태를 가진다.

 

세마포어의 값이 1이면 0으로 만들어 접근을 허용하고, 0이면 이미 다른 프로세스가 임계 영역을 사용 중이므로 대기한다.

그리고 임계 영역의 작업을 마친 프로세스가 나오면서 세마포어의 값을 증가시켜 다른 프로세스의 접근을 허용한다.

 

상호 배제

세마포어의 값이 0일 때는 다른 프로세스가 임계 영역에 접근할 수 없어 한 번에 하나의 프로세스만이 임계 영역에 접근할 수 있도록 한다.

 

Progress

세마포어 값이 1보다 커서 임계 영역이 사용 가능할 때 대기 중인 프로세스는 임계 영역에 진입할 수 있다.

 

Bounded Waiting

프로세스는 세마포어가 해제될 때까지만 대기하고 임계 영역을 사용한 후에는 세마포어를 반드시 해제하기 때문에 다른 프로세스가 공정하게 임계 영역에 접근할 기회를 얻는다.

 

Architectural Neutrality/Portability

대부분의 현대 운영 체제는 세마포어를 지원하지만, 세마포어의 구현과 사용 방법은 운영 체제에 따라 다르다.

따라서 Binary semaphore는 일정 수준의 이식성을 제공하지만, 완전한 구조적 중립성은 보장하지 않는다.