CS/운영체제

[CS/운영체제] 뮤텍스와 세마포어

연유뿌린빙수 2025. 10. 28. 17:34

하나의 CPU에서 여러 프로세스나 스레드가 동시 실행되는 환경에서는 공유 자원에 대한 동기 접근이 발생한다.

 

이러한 상황에서는 Race Condition과 같은 동시성 문제가 쉽게 발생하며, 시스템 오동작이나 데이터 불일치를 초래할 수 있다.

 

따라서 Critical Section(임계영역)에 대한 적절한 접근 제어는 필수적이며, 이를 위한 대표적인 기법이 바로 뮤텍스와 세마포어이다.


뮤텍스(Mutext)

Mutual Exclusiond -> Mutex

 

뮤텍스는 말 그대로 상호 배제(Mutual Exclusion)를 위한 동기화 원시(primitive)다. 공유 자원에 접근하려는 스레드는 반드시 락(lock)을 획득해야 하며, 락을 얻은 스레드만이 임계영역에 진입할 수 있다.

  • 락은 boolean 상태(locked / unlocked)로 관리되며
  • 오직 한 스레드만 접근을 허용한다
  • 락을 획득한 스레드만이 그것을 해제(release) 할 수 있다

cf. 뮤텍스 구현 방식에 따라 CPU 효율이 달라진다. ( 어떤 세션이 락을 확보하였다면, 락으로 인하여 대기를 하는 다른 과정에 대하여 어떻게 처리될지에 따라 장단점이 존재한다)

 

1) 스핀락(Spinlock) — Busy Waiting 방식

 

락을 얻지 못한 스레드가 계속 루프를 돌며 대기하는 방식이다.

- 장점: 문맥 교환(Context Switching) 없이 대기 → 임계영역이 짧으면 유리

- 단점: 대기 중에도 CPU 자원을 점유 → CPU 낭비

 

2) Blocking Mutex — Non-Busy Wait 방식

 

락 획득 실패 시 스레드는 대기 큐에 블록 상태로 이동한다.

- 장점: 대기 동안 CPU 자원을 소비하지 않음

- 단점: 스레드 sleep/wakeup 과정에서의 문맥 교환 오버헤드 발생


코드로 구현

java에는 concurrent 패키지의 locks에서 뮤텍스의 Lock 흐름을 쉽게 구현할 수 있다.

  • ReentrantLock
final boolean tryLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } else if (getExclusiveOwnerThread() == current) {
                if (++c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            }
            return false;
        }

final void lock() {
            if (!initialTryLock())
                acquire(1);
        }

 

$tryLock()$ 을 통하여 해당 세션에 대하여 Lock을 획보할 수 있는지 아닌지에 대하여 boolean 타입으로 반환을 해준다.

getState() 를 기반으로 현재가 Lock을 확보할 수 있는 상태라면 true를 반환하고, 아니라면 false를 반환한다.

 

lock() 메서드는 락을 반드시 획득할 때까지 대기하는 메서드로,

initialyTryLock() 을 통해 초기 시도에서 락을 획득할 수 있는지 아닌지를 확인하고 acquire(1)을 호출하여 락을 획득할 때까지 기다린다.

 

  • ReentrantLock 객체를 기반으로 구현한 뮤텍스
import java.util.concurrent.locks.ReentrantLock;

public class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int value = 0;

    public void increment() {
        lock.lock();
        try {
            value++;
        } finally {
            lock.unlock(); // 예외 발생해도 락 해제 보장
        }
    }

    public int get() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SafeCounter c = new SafeCounter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) c.increment();
        });

        t1.start(); t2.start();
        t1.join();  t2.join();

        System.out.println("final = " + c.get()); // 200000
    }
}
  • increment()
public void increment() {
	lock.lock();
    try {
    	value++;
    } finally {
    	lock.unlock();
    }
}

 

lock을 걸고 value를 증가 시킨다.

그러면 다른 프로세스의 개입 없이 value를 증가시킬 수 있다.

또한 만약 lock을 해제하지 못할 경우 해당 스레드/프로세스가 점유를 계속 하게 되기 때문에 데드락이 발생할 수 있으므로 finally를 통하여 확보한 락을 최종적으로 해제한다. (count를 둬서 timeout일 경우에 unlock을 처리하는 코드를 추천)

  • get()
public int get() {
	lock.lock();
    try {
    	return value;
    } finally {
    	lock.unlock();
    }
}

 

조회를 할 때 다른 프로세스나 스레드의 개입이 없도록 조회를 실행한 시점에서의 값을 반환해준다.
(이 부분은 GET_LOCK의 메서드와 동일 )



세마포어

세마포어는 공유 자원에 접근 가능한 동시 허용 개수를 정수 값으로 관리하는 동기화 도구다. 내부적으로 P(wait) / V(signal) 연산을 통해 값이 감소 또는 증가하며 자원 접근 가능 여부를 제어한다.

  • 0인 경우: 접근 불가 → 스레드는 대기 큐로 이동(blocking)
  • N > 0인 경우: 최대 N개의 프로세스/스레드 동시 접근 가능
  • Non-Busy-Wait 방식으로 동작 ( 위에서 서술한 락 획득 실패 시 스레드는 대기 큐에 블록 상태로 이동하는 부분 )

세마포어 종류

  • 이진 세마포어(Binary Semaphore) : 값이 0 또는 1 — 기능적으로 뮤텍스와 유사
  • 카운팅 세마포어(Counting Semaphore) : 동시에 접근 가능한 스레드 수를 정수로 관리

굉장히 많음!

코드로 구현

Java의 Sempaphore 객체를 이용하여 구현한다.

  • Semaphore 생성자
public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

 

permits를 통하여 최대로 허용하는 동시 접속 가능한 스레드 및 프로세스의 수를 정한다( 현재 허용 가능한 자원의 수)

fair boolean 타입 변수를 통하여 세마포어가 공정하게 동작할지에 대하여 결정한다.

fair access를 보장하지 않을 경우 성능에 영향을 갈 수 있다.

 

Semaphore 객체를 통하여 구현한 코드를 확인해보자.

import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom;

public class BoundedResource {
    private final Semaphore permits = new Semaphore(3, true); // 공정 모드(optional)

    public void use(String name) {
        try {
            permits.acquire();              // 남은 퍼밋 없으면 대기(block)
            try {
                System.out.println(name + " ENTER");
                // 임계구역: 실제 자원 사용
                Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300));
                System.out.println(name + " EXIT");
            } finally {
                permits.release();          // 사용 후 퍼밋 반환
            }
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        BoundedResource res = new BoundedResource();
        for (int i = 1; i <= 10; i++) {
            final String worker = "W" + i;
            new Thread(() -> res.use(worker)).start();
        }
    }
}
  • 직접 정의한 use(String name)
public void use(String name) {
	try {
    	permits.acquire();
        try {
        	System.out.println(name + " ENTER");
            Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300));
            System.out.println(name + " EXIT");
        } finally {
        	permits.release();
        }
    } catch (InterruptedException e) {
    	Thread.currentThread().interrupt();
    }
}

 

permits가 acquire 메서드를 통하여 퍼밋을 획득하려고 시도한다.

만약 세마포어에서 사용가능한 남은 퍼밋이 존재하지 않으면 Non busy waiting 으로 대기 큐로 들어간다.

 

이 때 acquire()는 세마포어에서 퍼밋이 반환될 때까지 블록되어 대기하고, 다음 try 안에서 퍼밋을 확보하면 리소스에 접근하여 실행하는 코드이다.


하지만 대기 큐에 데드락이 걸려서는 안되기 때문에 finally 구문에 release() 메서드를 실행해주어야한다.

 

또한 최종적으로 에러가 일어난다면 스레들르 직접 종료하는 형식으로 코드를 작성해준다.

 

 

비교 정리

뮤텍스 : 락, 한 개의 프로세스/스레드 접근
세마포어 : 공유 변수, N개의 프로세스/스레드 접근

 

뮤텍스를 락을 소유한 프로세스/스레드만 해제할 수 있다.

그러나 세마포어는 다른 스레드/프로세스에서도 작업을 처리할 수 있다.

 

뮤텍스와 이진 세마포어는 비슷하지만 다르다.

 

뮤텍스는 세마포어로 사용될 수 없지만, 세마포어는 뮤텍스로 사용이 가능하다.

 

 

그렇기 때문에,
1개의 자원에 대한 상호배제만 필요하다면 뮤텍스를 사용하고
공유자원 접근 수를 조절하고 작업 간의 싈행순서 동기화가 필요하다면 세마포어를 사용한다.