Computer Science/Operating System

경쟁상태 해결하기 - 뮤텍스, 세마포어, 모니터

settong 2024. 11. 1. 19:24
반응형

경쟁상태 해결 조건

여러 프로세스나 스레드가 공유 자원에 접근할 때 데이터의 일관성과 무결성을 보장하기 위해 아래와 같은 조건을 만족해야한다.

 

상호배제 (Mutual Exclusion)

임계영역에 한번에 하나의 프로세스나 스레드만 접근할 수 있도록 해야함.

즉, 어떤 프로세스가 임계영역에 있을 때 다른 프로세스는 그 임계영역에 들어갈 수 없음.

 

진행의 융통성(Progress)

임계영역에 들어가려는 프로세스가 없으면, 임계영역에 들어갈 수 있는 프로세스가 이를 결정할 수 있어야 함.

즉, 임계영역에 들어갈 수 있는 프로세스를 선택하는 과정이 지연되지 않고 진행되어야 함.

결정은 임계영역에 있지 않은 프로세스만 참여할 수 있음.

 

한정 대기 (Bounded Wating)

특정 프로세스가 임계영역에 들어가는 것이 무한정 지연되면 안됨.

어떤 프로세스가 임계영역에 들어가려고 요청한 이후, 그 프로세스가 임계영역에 들어갈 수 있을 때까지 다른 프로세스들이 임계영역에 들어가는 횟수는 제한시켜 특정 프로세스가 영원히 임계영역에 들어가지 못하는것을 방지.

 

 

이 조건을 잘 지키며, 공정성을 잘 지켜지는 경쟁상태를 해결하는 대표적 방법으로는 뮤텍스, 세마포어, 모니터가 있다.

공정성? 여러 스레드나 프로세스가 경쟁상태에 있을 때 특정 스레드나 프로세스가 불공평하게 오래 기다리지 않도록 하는 성질.

 

 

 

 

뮤텍스 (Mutex)

객체 lock을 기반으로 경쟁상태를 해결함
뮤텍스를 lock()을 통해 잠금 설정하고 사용한 후에 unlock()을 통해 잠금해제가 됨.

한 프로세스나 스레드가 잠금을 설정하면 다른 프로세스나 스레드는 잠긴 코드 영역에 접근할 수 없다.

뮤텍스가 잠금된 것을 뮤텍스를 획득한다고 함.

 

Java에서 뮤텍스 예제

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexExample {
    private int counter = 0;
    private final Lock lock = new ReentrantLock(); // ReentrantLock 객체를 생성하여 뮤텍스 초기화

    public void increment() {
        lock.lock();  // 뮤텍스 획득(잠금)
        try {
            counter++;  // 임계 영역
        } finally {
            lock.unlock();  // 뮤텍스 해제
        }
    }

    public int getValue() {
        return counter;
    }

    public static void main(String[] args) {
        MutexExample example = new MutexExample();
		
        // task를 실행하면 increament() 메서드를 100000번 실행
        Runnable task = () -> {
            for (int i = 0; i < 100000; i++) {
                example.increment();
            }
        };
		
        // thread1, thread2 두개의 스레드가 task 실행
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();

        try {
            thread1.join();  // thread1의 작업이 완료될 때까지 기다림
            thread2.join();  // thread2의 작업이 완료될 때까지 기다림
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + example.getValue());
    }
}

ReentrantLock은 Java에서 제공하는 lock객체이다. 일종의 뮤텍스로 ReentrantLock은 스레드가 여러번 잠금을 획득할 수 있는 기능(재진입 기능)을 제공한다. synchronized에서는 제공하지 않는 공정성 옵션을 제공한다. 공정모드에서는 대기중인 스레드가 순서대로 락을 획득한다.

 

 

 

 

세마포어 (Semaphore)

여러 프로세스나 스레드가 동시에 임계영역에 접근할 수 있다. <- 상호배제를 명시적으로 구현해야함.

대신 임계영역에 접근할 수 있는 최대 수를 제어함으로써, 경쟁상태를 해결한다.

(예시, DB연결 커넥션)

일반화된 뮤텍스. 간단한 정수 S와 wait(), signal() 메서드로 공유 자원에 대한 접근을 처리한다.

  • S는 현재 쓸 수 있는 공유자원의 수.
  • wait()은 S를 1씩 감소시킴. 자원 요청 시 호출.
    • 자원 요청을 위해 wait()을 호출했을 때 S가 음수가 되면? 사용 가능한 자원이 없다는 뜻. 공유자원을 쓸 수 없음.
      -> 접근 요청한 프로세스는 준비 큐에 집어 넣고 자원이 해제될 때까지 기다림.
    • S가 0 이상이면 프로세스는 자원을 사용할 수 있다는 뜻. 아무런 작업을 하지 않고 반환.
  • signal()은 S를 1씩 증가시킴. 자원을 해제 시 호출.
    • 자원 해제를 위해 signal()을 호출했을 때 S가 0이하라면? 준비 큐에 대기 중인 프로세스가 있다는 얘기.
      -> 대기열에 있던 프로세스를 끄집어 내서 공유자원에 대한 작업을 진행 함.
    • S가 0보다 크면 대기중인 프로세스가 없다는 뜻. 아무런 작업을 하지 않고 반환.

 

Java에서 세마포어 예제

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int MAX_PERMITS = 1; // 최대 1개의 자원 허용
    private final Semaphore semaphore = new Semaphore(MAX_PERMITS); // 세마포어 생성

    public void accessResource() {
        try {
            semaphore.acquire(); // wait() 연산: 세마포어 값을 1 감소시키고, 세마포어 값이 음수면 대기열에 추가
            System.out.println(Thread.currentThread().getName() + " is accessing the resource");
            // 자원에 접근
            Thread.sleep(1000); // 자원을 사용하는 시간 (예: 파일 읽기/쓰기)
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            System.out.println(Thread.currentThread().getName() + " is releasing the resource");
            semaphore.release(); // signal() 연산: 세마포어 값을 1 증가시키고, 대기열에서 프로세스 깨움
        }
    }

    public static void main(String[] args) {
        SemaphoreExample example = new SemaphoreExample();

        Runnable task = example::accessResource;

        Thread thread1 = new Thread(task, "Thread 1");
        Thread thread2 = new Thread(task, "Thread 2");

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

자바에서는 acquire()가 wait() 역할을 하고, release()가 signal()역할을 한다.

 

 

세마포어 종류

  • 이진(바이너리) 세마포어
    • 세마포어가 0,1 두가지 값만 가질 수 있음.
    • 뮤텍스와 유사하게 동작하지만 뮤텍스는 '잠금 메커니즘', 세마포어는 '신호 메커니즘'
  • 카운팅 세마포어
    • 여러개의 값을 가질 수 있는 세마포어.

 

 

 

 

모니터 (Monitor)

 

공유자원을 숨기고 해당 접근에 대해 인터페이스만 제공하는 객체이다.

언어수준에서 제공되는 동기화 수단이다. 고수준의 동기화 수단이다.

ㄴ 세마포어나 뮤텍스보다 사용하기 쉽고 오류가 적다.

뮤텍스와 조건변수를 결합한 형태이다.

한번에 하나의 프로세스만 공유자원에 접근할 수 있다. <- 상호배제가 자동.

모니터는 '공유변수', '동기화된 메서드', '조건변수'로 구성된다.

  • 공유변수
    모니터 내에서 관리되고 보호되는 변수들. 외부에서 직접 접근 불가.
  • 동기화된 메서드(Procedure/Function)
    공유자원에 접근/조작하는데 사용되는 메서드. 한번에 하나의 프로세스/스레드만 실행 가능한 메서드.
  • 조건변수
    프로세스가 특정 조건을 기다리거나 신호를 보낼 때 사용. wait(), signal(), notify(), notifyAll() 등의 연산을 지원.

 

 

Java에서 모니터 예제

class MonitorExample {
    private int count = 0;
    
    /*
    아래 함수들을 synchronized키워드에 의해
    한 스레드가 count를 변경중이면 다른 스레드가 count를 변경하지 못하게 하고
    동기화되어 있으므로 변경되는 동안 값을 안전하게 읽을 수 있게 함.
    */
    public synchronized void increment() {
        count++;
    }
    public synchronized void decrement() {
        count--;
    }
    public synchronized int getCount() {
        return count;
    }

    // wait와 notify를 사용한 동기화 블록
    public void exampleMethod() {
     	// 아래 블록은 한번에 하나의 스레드만 진입 가능. 특정 조건이 만족할 때 까지 대기.
        synchronized (this) {
            while (count == 0) { // count가 0일때는 대기
                try {
                    wait(); // 현재 스레드를 일시정지 상태로 만듦
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 인터럽트 발생 시 스레드를 활성화
                }
            }
            // count를 사용하여 어떤 작업 수행
            count--;
            notify(); // 대기 중인 다른 스레드에 알림
        }
    }
}

자바에서는 synchronized 키워드와 wait(), notify(), notifyAll() 메서드를 사용하여 모니터를 구현할 수 있다.

synchronized 키워드를 사용하여 increment(), decrement(), getCount() 메서드를 동기화한다. 한번에 하나의 스레드만 이 메서드들 을 실행할 수 있다.

728x90
반응형