STUDY

[운영체제] 동기화 도구: 뮤텍스 락, 세마포, 모니터 정리

sed 2026. 5. 27. 16:14
SMALL

동기화 도구: 뮤텍스 락, 세마포, 모니터

이전 글에서 프로세스 동기화가 필요한 이유를 살펴보았다.

여러 프로세스나 스레드가 공유 자원에 동시에 접근하면 데이터의 일관성이 깨질 수 있다.  

 

이를 막기 위해 운영체제는 동기화 도구를 제공한다.

 

대표적인 동기화 도구에는 다음과 같은 것들이 있다.

 

 

뮤텍스락

뮤텍스 락(Mutex Lock)은 상호 배제를 위한 동기화 도구이다.

 

상호 배제란 한 번에 하나의 프로세스만 임계 구역에 들어갈 수 있도록 하는 것이다.

 

뮤텍스 락은 쉽게 말하면 자물쇠 역할을 한다.

 

옷가게 탈의실을 생각해보자.

 

탈의실은 한 번에 한 명만 사용할 수 있다.
누군가 탈의실을 사용 중이라면 다른 사람은 밖에서 기다려야 한다.

손님 = 프로세스
탈의실 = 임계 구역
자물쇠 = 뮤텍스 락

 

탈의실 문이 잠겨 있다면 누군가 사용 중이라는 뜻이다.
문이 열려 있다면 들어가서 문을 잠그고 사용하면 된다.

뮤텍스 락도 이와 비슷하다.

프로세스가 임계 구역에 들어가기 전에 lock 상태를 확인한다.
잠겨 있으면 기다리고, 잠겨 있지 않으면 lock을 걸고 임계 구역에 들어간다.

뮤텍스 락의 기본 구조

뮤텍스 락은 간단하게 보면 전역 변수 하나와 함수 두 개로 표현할 수 있다.

lock 변수
acquire 함수
release 함수

 

lock은 임계 구역이 잠겨 있는지 나타내는 전역 변수이다.

lock = true  → 임계 구역이 잠겨 있음
lock = false → 임계 구역이 비어 있음

 

acquire()는 임계 구역에 들어가기 전에 호출하는 함수이다.
임계 구역이 비어 있다면 잠그고 들어가고, 이미 잠겨 있다면 기다린다.

 

release()는 임계 구역에서 나온 뒤 호출하는 함수이다.
임계 구역 사용이 끝났으니 lock을 해제한다.

 

의사 코드는 다음과 같다.

acquire() {
    while (lock == true)
        ;

    lock = true;
}

release() {
    lock = false;
}

 

프로세스는 임계 구역에 들어가기 전에 acquire()를 호출한다.

acquire();

// 임계 구역

release();

 

즉, 전체 흐름은 다음과 같다.

1. 임계 구역에 들어가기 전 acquire() 호출
2. lock이 false라면 lock을 true로 바꿈
3. 임계 구역 실행
4. 임계 구역을 빠져나오며 release() 호출
5. lock을 false로 바꿈

 

이렇게 하면 한 프로세스가 임계 구역에 들어가 있는 동안 다른 프로세스는 들어갈 수 없다.

바쁜 대기

위의 뮤텍스 락 구현에는 문제가 있다.

while (lock == true)
    ;

 

이 코드는 lock이 풀릴 때까지 계속 반복해서 확인한다.

 

즉, 프로세스가 실제로 할 일은 없지만 CPU를 사용하면서 계속 lock 상태만 검사한다.

 

이런 대기를 바쁜 대기(busy waiting)라고 한다.

 

바쁜 대기는 CPU를 낭비할 수 있다.
기다리는 동안 차라리 CPU를 다른 프로세스에게 넘기는 편이 더 효율적일 수 있기 때문이다.

 

 

세마포

세마포(Semaphore)는 뮤텍스 락보다 더 일반화된 동기화 도구이다.

뮤텍스 락은 보통 하나의 공유 자원에 대해 한 번에 하나의 프로세스만 접근하게 할 때 사용한다.

반면 세마포는 공유 자원이 여러 개 있는 경우에도 사용할 수 있다.

 

예를 들어 탈의실이 하나라면 뮤텍스 락으로 충분하다.

하지만 탈의실이 3개라면 어떨까?

 

이 경우에는 사용 가능한 자원의 개수를 세야 한다.
이때 사용할 수 있는 것이 세마포이다.

 

참고로 세마포는 크게 두 가지로 볼 수 있다.

이진 세마포 binary semaphore
카운팅 세마포 counting semaphore

 

이진 세마포는 값이 0 또는 1만 가능하므로 뮤텍스 락과 비슷하게 사용할 수 있다.

카운팅 세마포는 0, 1, 2, 3처럼 여러 값을 가질 수 있고, 사용 가능한 자원의 개수를 나타낼 수 있다.

여기서는 카운팅 세마포를 기준으로 이해하면 된다.

세마포의 기본 구조

세마포는 전역 변수 하나와 함수 두 개로 설명할 수 있다.

S
wait()
signal()

 

S는 사용 가능한 공유 자원의 개수를 나타내는 변수이다.

 

wait()는 임계 구역에 들어가기 전에 호출하는 함수이다.
자원이 남아 있으면 자원을 하나 사용하고 들어간다.
자원이 없다면 기다린다.

 

signal()은 임계 구역을 빠져나온 뒤 호출하는 함수이다.
사용이 끝난 자원을 반납한다.

 

기본 의사 코드는 다음과 같다.

wait() {
    while (S <= 0)
        ;

    S--;
}

signal() {
    S++;
}

 

사용 흐름은 다음과 같다.

wait();

// 임계 구역

signal();

 

예를 들어 S = 3이라면 처음에는 세 프로세스까지 임계 구역에 들어갈 수 있다.

첫 번째 프로세스 wait() → S = 2
두 번째 프로세스 wait() → S = 1
세 번째 프로세스 wait() → S = 0
네 번째 프로세스 wait() → S <= 0이므로 대기

 

임계 구역을 빠져나온 프로세스가 signal()을 호출하면 S가 증가한다.

signal() 호출 → S = 1
기다리던 프로세스가 다시 들어갈 수 있음

 

세마포와 바쁜 대기 문제

위의 세마포 구현도 뮤텍스 락처럼 바쁜 대기 문제가 있다.

사용 가능한 자원이 생길 때까지 계속 반복해서 확인하기 때문이다.

 

이를 해결하기 위해 실제 구현에서는 기다리는 프로세스를 대기 상태로 만들 수 있다.

 

자원이 없으면 해당 프로세스를 대기 큐에 넣고 잠들게 한다.
자원이 생기면 대기 큐에서 프로세스를 꺼내 준비 상태로 만든다.

 

개선된 의사 코드는 다음과 같다.

wait() {
    S--;

    if (S < 0) {
        add this process to Queue;
        sleep();
    }
}

signal() {
    S++;

    if (S <= 0) {
        remove a process p from Queue;
        wakeup(p);
    }
}

 

이 방식에서는 자원이 없을 때 프로세스가 계속 CPU를 사용하며 기다리지 않는다.

 

대신 대기 큐에 들어가 잠들고, 나중에 자원이 생기면 다시 깨워진다.

자원 없음
→ 대기 큐에 삽입
→ sleep

자원 생김
→ 대기 큐에서 꺼냄
→ wakeup

 

따라서 바쁜 대기보다 CPU를 더 효율적으로 사용할 수 있다.

세마포를 활용한 실행 순서 제어

세마포는 상호 배제뿐만 아니라 실행 순서 제어에도 사용할 수 있다.

 

예를 들어 프로세스 A가 먼저 실행되고, 그 다음에 프로세스 B가 실행되어야 한다고 하자.

이때 세마포 변수 S를 0으로 둔다.

그리고 먼저 실행해야 하는 프로세스 A의 끝부분에 signal()을 둔다.

A() {
    // A가 먼저 해야 할 작업

    signal();
}

 

나중에 실행해야 하는 프로세스 B의 앞부분에는 wait()를 둔다.

B() {
    wait();

    // B가 해야 할 작업
}

 

처음에는 S = 0이므로 B가 먼저 실행되더라도 wait()에서 대기한다.

 

A가 실행을 마치고 signal()을 호출하면 S가 증가하고, B는 그때부터 실행될 수 있다.

 

즉, 세마포를 이용하면 “A가 끝난 뒤 B가 실행되어야 한다”와 같은 실행 순서도 제어할 수 있다.

다만 세마포는 개발자가 직접 wait() signal()을 적절한 위치에 호출해야 한다.

이 순서가 헷갈리거나, 호출을 빠뜨리거나, 중복 호출하면 문제가 생길 수 있다.
그래서 세마포는 강력하지만 사용이 번거롭고 디버깅이 어려울 수 있다.

 

 

모니터

세마포의 불편함을 줄이기 위해 등장한 동기화 도구가 모니터(Monitor)이다.

모니터는 개발자가 더 편하게 사용할 수 있도록 만든 고수준 동기화 도구이다.

모니터는 상호 배제를 위한 동기화, 실행 순서 제어를 위한 동기화 두 가지를 모두 지원할 수 있다.

 

모니터와 상호 배제

모니터는 공유 자원과 그 공유 자원에 접근하는 인터페이스를 함께 관리한다.

즉, 공유 자원에 아무나 직접 접근하는 것이 아니라, 모니터가 제공하는 특정 함수나 메서드를 통해서만 접근하게 한다.

 

모니터의 중요한 특징은 모니터 안에는 한 번에 하나의 프로세스만 들어갈 수 있다는 점이다.

즉, 어떤 프로세스가 모니터 내부의 코드를 실행 중이라면, 다른 프로세스는 모니터 안으로 들어갈 수 없다.

 

이 구조 덕분에 모니터는 기본적으로 상호 배제를 보장할 수 있다.

개발자는 세마포처럼 매번 임계 구역 앞뒤에 wait()와 signal()을 직접 배치하지 않아도 된다.
모니터가 한 번에 하나의 프로세스만 내부로 들어오도록 관리하기 때문이다.

 

모니터와 실행 순서 제어

모니터는 실행 순서 제어를 위해 조건 변수(condition variable)를 사용한다.

조건 변수는 특정 조건이 만족될 때까지 프로세스나 스레드를 기다리게 하기 위한 특별한 변수이다.

조건 변수에는 보통 두 가지 연산이 있다.

wait()
signal()

 

여기서의 wait()와 signal()은 세마포의 wait, signal과 비슷해 보이지만, 모니터 내부의 조건 변수에 대해 사용된다는 점에서 구분해서 이해하면 좋다.

 

조건 변수의 wait()는 현재 프로세스를 대기 상태로 만들고, 해당 조건 변수의 큐에 넣는다.

조건 변수의 signal()은 조건 변수에서 기다리고 있는 프로세스 중 하나에게 실행 가능하다는 신호를 보낸다.

 

예를 들어 버퍼가 비어 있는데 소비자가 데이터를 꺼내려고 한다고 하자.

 

소비자는 꺼낼 데이터가 없으므로 기다려야 한다.

이후 생산자가 데이터를 넣으면, 소비자를 깨울 수 있다.

 

즉, 조건 변수는 특정 조건이 만족될 때까지 기다리고, 조건이 만족되면 다시 실행되게 하는 도구이다.

 

모니터에서 wait와 signal의 동작

모니터 안에는 한 번에 하나의 프로세스만 들어올 수 있다.

그런데 어떤 프로세스가 모니터 안에서 wait()를 호출하면 어떻게 될까?

 

이 프로세스는 조건 변수 큐로 들어가 대기 상태가 되고, 모니터 밖으로 나간다.

그래야 다른 프로세스가 모니터에 들어와 조건을 바꿀 수 있다.

 

예를 들어 소비자가 모니터에 들어왔는데 버퍼가 비어 있다면, 소비자는 wait()를 호출하고 기다린다.
그 후 생산자가 모니터에 들어와 버퍼에 데이터를 넣고 signal()을 호출한다.

이제 기다리던 소비자는 다시 실행될 수 있다.

 

정리하면 다음과 같다.

wait()
- 조건이 만족되지 않았을 때 호출
- 현재 프로세스를 대기 상태로 만듦
- 조건 변수 큐에 삽입
- 모니터를 비움

signal()
- 조건이 만족되었을 때 호출
- 조건 변수 큐에서 기다리는 프로세스 하나를 깨움

 

뮤텍스 락, 세마포, 모니터 비교

세 가지 도구를 비교하면 다음과 같다.

뮤텍스 락
- 한 번에 하나의 프로세스만 임계 구역에 들어가게 함
- 자물쇠와 비슷함
- acquire(), release() 사용
- 단순하지만 바쁜 대기가 발생할 수 있음

세마포
- 공유 자원이 여러 개일 때도 사용 가능
- wait(), signal() 사용
- 상호 배제와 실행 순서 제어 모두 가능
- 사용자가 호출 위치를 직접 관리해야 해서 실수하기 쉬움

모니터
- 공유 자원과 접근 함수를 함께 관리
- 한 번에 하나의 프로세스만 모니터 내부에 진입 가능
- 조건 변수로 실행 순서 제어 가능
- 세마포보다 사용하기 편한 고수준 동기화 도구

 

LIST