[OS] 뮤텍스와 세마포어

 

 

안정된 동기화 매커니즘의 필요성


컴퓨터는 프로세서, 즉 CPU의 성능을 극대화하기 위해 동시에 여러 작업을 수행하려고 한다. 이를 위해 멀티스레딩과 동시성 프로그래밍 기법이 발전해왔다. 그런데 여러 스레드가 동시에 실행된다는 것은 자원 공유에 대한 문제를 고려해야하는 일이다. 가령 두 사람이 같은 은행 계좌에서 동시에 출금을 시도했는데 결과 잔액이 음수가 되는 사항을 막기 위해서가 대표적인 예다. 따라서 안정된 동기화 매커니즘을 맞추기 위해 뮤텍스와 세마포어가 등장했으며, 스레드 간 경쟁을 방지하고 공유 자원의 무결성을 보장하는 것이 그 목적이다. 그리고 각각은 다음과 같은 방법을 취한다.

 

😡 뮤텍스: 한 번에 한 명만 들어오라구.

🤢 세마포어: 한 번에 이만큼만 들어오라구.

 

 

 

뮤텍스(Mutex)


뮤텍스는 보통 열쇠로 비유된다. 문을 잠그고 열 수 있는 열쇠로서, 한 사람이 문을 열고 들어가면 다른 사람은 기다려야한다. 들어간 사람이 열쇠를 가지고 뒷사람에 전달해주면 그 뒷사람이 이용할 수 있게 되는 것이다.

그리고 뮤텍스는 이진 잠금(Binary Lock)을 사용하여 아래의 두 가지 상태만을 가리킨다.

- 잠금 상태(1, Locked): 한 스레드가 자원을 이용 중

- 해제 상태(0, Unlocked): 자원이 비어 있어 다른 스레드가 접근 가능

 

이렇듯 뮤텍스는 하나의 스레드만 해당 자원을 사용할 수 있는 독점적 보호를 제공한다. 목적 자체가 동시 접근으로 인한 데이터의 손실 위험을 막고자하는 단일 자원의 무결성 보장이기 때문이다. 이러한 데이커 무결성 보장을 위하여 단일 스레드만 접근 가능하도록 보장함과 동시에 소유권 개념을 동반하여 잠금을 걸었던 스레드만 잠금을 해제할 수 있도록 한다. 앞의 비유를 통하며 내가 이용중인 사물함을 다시 열 수 있는 건 나만이 가능해야되는 거다.

 

얘를 Java에서 사용해보면 Syscnronized 키워드를 쓰거나 ReentrantLock을 이용하는 방법이 있다.

가령 예시로 은행 계좌에서 잔액을 수정하는 코드가 존재한다고 치자.

한 스레드가 작업을 끝내기 전에 다른 스레드가 접근하면 경쟁 조건(Race Condition)이 발생한다. 작업 중에는 다른 스레드가 접근하지 못하도록 이 뮤텍스 기법으로 보호해야한다.

public class BankAccount {
    private int balance = 0;

    public synchronized void deposit(int amount) {
        balance += amount; // 한 스레드만 이 코드를 실행 가능
    }

    public synchronized int getBalance() {
        return balance; // 한 스레드만 접근 가능
    }
}

 

 

 

세마포어(Semaphore)


공용 주차장을 떠올려보자. 세마포어는 이 여러 칸이 있는 공용 주차장이다. 3칸이 있으면 최대 차량 3대까지 동시에 사용할 수 있고, 4번째 차량은 누군가가 나오기까지 기다려야 한다. 이렇듯 '몇 개'라는 카운터를 기반으로 자원의 접근을 제어하게 되는데, 카운터 값이 1이라면 뮤텍스처럼 이진 세마포어로 두 가지 상태로 동작하며, 2 이상이라면 여러 스레드가 동시에 자원을 사용하는 형태로 동작하여 유연성을 가진다.

 

뮤텍스와 비교하면 세마포어는 자원의 효율적 분배를 목표로한다. 여러 스레드가 동시에 자원을 사용해도 안전하다면, 제한 개수만큼 공유하므로써 활용도를 높이고 싶은 것이다. 이 세마포어는 소유권 개념은 없다. 따라서 모든 스레드가 세마포어를 해제할 수 있는데, 이 특징이 여러 스레드가 자원 공유 시 유용해지는 것이다.

 

Java 내의 Semaphore 클래스를 이용하면 아래와 같다.

import java.util.concurrent.Semaphore;

public class ParkingLot {
    private final Semaphore semaphore;

    public ParkingLot(int slots) {
        semaphore = new Semaphore(slots); // 주차 공간 개수 설정
    }

    public void park() throws InterruptedException {
        semaphore.acquire(); // 주차 공간 확보
        System.out.println("차가 주차되었습니다: " + Thread.currentThread().getName());
    }

    public void leave() {
        semaphore.release(); // 주차 공간 반환
        System.out.println("차가 나갔습니다: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ParkingLot lot = new ParkingLot(3); // 주차 공간 3개

        Runnable car = () -> {
            try {
                lot.park();
                Thread.sleep(2000); // 주차 후 잠시 대기
                lot.leave();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int i = 0; i < 5; i++) { // 5대의 차가 주차 시도
            new Thread(car).start();
        }
    }
}

 

 

이 친구의 실제 예시는 데이터베이스 연결 풀이다.

데이터 베이스 연결은 제한된 수의 연결 자원을 갖고 있는데, 여러 스레드가 연결을 요청하면 세마포어로 자원의 개수를 관리한다.

 

 

정리


뮤텍스와 세마포어는 문제를 해결하려는 관점이 다르다

뮤텍스는 "내가 자원을 보호해야해" 라는 독점적 보호 철학을 가진다면, 세마포어는 "같이 써도 되는데, 질서는 지켜" 라는 효율적 분배를 목표로 한다.