Programming/Process & Multi Threading

Mutext 뮤텍스 vs Semaphore 세마포어

dev.pudding 2024. 1. 9. 21:49
728x90

글에 들어가기에 앞서, 세마포어와 뮤텍스의 가장 기본적인 차이점은 세마포어는 signal mechanism 신호 메커니즘이라는 것이다. 프로세스는 자원을 획득하기 위해  wait()과 signal()을 이용하여 작업을 수행한다. 반면에 뮤텍스는 lock mechanism 잠금 메커니즘이다. 프로세스가 리소스를 획득하려면 해당 뮤텍스 객체의 락을 얻어야한다. 

세마포어란?

세마포어는 한국어로 '신호기'라는 의미로서, 마치 기찻길의 신호기처럼 두 개의 철도가 충돌하는 것을 막기위해 두 열차의 진입순서를 막아주는 역할을 한다.

Semaphore(int permits, boolean fair)

 

신호기의 역할은 Semaphore 생성자의 파라미터로 들어가는 int permits이 한다. permits은 세마포어가 가지는 초기 공유자원의 허용 권한의 갯수를 나타내며, 몇 개의 스레드가 동시에 작업을 수행할 수 있는지를 나타낸다.이제부터 permits의 값이 저장된 변수를 S라고 부르겠다.(S는 세마포어 변수를 부르는 통상적인 명칭으로 사용된다)

예를들어 S의 값이 1이 들어가게 된다면, 사용가능한 공유자원은 1개라는 의미이다. 만약 누군가가 한 개의 공유자원을 점유하게 된다면 S의 값은 0이 되고 스레드는 대기상태가 된다. 

 

대기상태에 들어간 스레드는 List 자료구조에 들어가게 된다. 자원이 사용가능해지면 리스트에 들어있는 값을 사용하게 되는데, boolean fair 파라미터에 true 값을 넣어주면  first in first out방식을 사용하여, 제일 처음 리스트에 들어갔던 스레드를 실행시킨다. 반면에 boolean fair 파라메터에 false 값을 넣어주면 스레드 선택이 무작위로 진행되는데 이는 운영체제의 스케줄러 정책에 의존하기 때문이다. 

 

세마포어는 신호 메커니즘(signal mechanism)으로 작용하며, 세마포어를 기다리는 스레드는 다른 스레드에 의해 신호를 받을 수 있다. 자바에서 세마포어는 acquire()와 release() 메소드를 사용하여 프로세스 동기처리를 한다. 이 작업은 세마포어의 값을 원자적으로 수정한다. 이는 한 프로세스가 세마포어의 값을 변경하는 경우, 다른 프로세스가 동시에 세마포어의 값을 변경할 수 없음을 의미한다.

 

 

세마포어 사용예시

세마포어는 S에 들어간 값으로 허용 가능한 공유자원의 갯수를 추적하며, 이를 통해 공유 자원에 대한 접근을 조절한다. 세마포어는 크게 counting semaphore와 binary semaphore(이진세마포어)로 나누어지는데, 둘의 차이는 간단하다. 만약 1과 0으로만 S가 이루어지면 binary semaphore이고, S가 0개부터 N개인 경우를 counting semaphore라고 한다. 아래의 예시는 S가 여러개인 counting semaphore의 예시이다.

acquire() : Lock을 확보하는 메소드이다. 

release(): 확보했던 Lock을 세마포어에게 돌려주는 메소드이다.

enum Downloader{ // 싱글톤 패턴이며 INSTANCE 변수를 통해 접근할 수 있다. 

	INSTANCE;     
	private Semaphore semaphore = new Semaphore(3,true); //사용가능 공유자원 3개, 스레드는 FIFO방식으로 선택된다. 
    
    public void download(){
    		try{
               semaphore.acquire(); //Lock 확보
               downloadData();
            }catch(InterruptedException e){
            	e.printStackTrace(); 
            }finally{ // 무조건 실행되는 구간, Lock 확보 후 다시 돌려준다. 
            	semaphore.release(); //Lock 돌려줌 
            }
    }
    
    private void downloadData(){
           try{
              Thread.sleep(2000); //2초마다 다운로드 
              System.out.println("Download data ");
            }catch(InterruptedException e){
            	e.printStackTrace();
            }
    }
    
}

public class App{
	public static void main(String[] args){
    
    	ExecutorService service = Executors.newCachedThreadPool();
    
    	for(int i=0;i<12;i++){ //12개의 스레드를 생성하여 동시접근
        	service.execute(new Runnable(){
            	@Override
                public void run(){
                	Downloader.Instance.Download();
                }
            });
        }
    }
}

 

출력결과는 2초마다 Download data 가 3개씩 프린트된다. 12개의 스레드가 동시에 공유자원에 접근하여도, 세마포어로 인해 한번에 접근 가능 공유자원의 개수는 3개이기 때문이다. 

 

뮤텍스란?

Mutex의 전체 명칭은 Mutual Exclusion Object로, 공유된 자원에 대한 접근을 제어하기 위해 사용되는 이진 세마포어의 특수한 형태이다.  뮤텍스는 잠금 메커니즘(locking mechanism)으로서 스레드나 프로세스가 자원에 접근할 때 뮤텍스 객체에서 lock을 얻고 사용해야한다. 뮤텍스는 우선순위 상속 메커니즘을 포함하고 있으며, 우선순위 반전에 따른 **Race Condition 문제를 해결할 수 있다.

 

**Race Condition : 입력 변화의 타이밍이나 순서가 예상과 다르게 작동하면 정상적인 결과가 나오지 않는데 이를 Race Condition이라고 한다. 

 

뮤텍스 사용예시

public class App{
	private static int counter = 0;
	private static Lock lock = new ReentrantLock();  // 재진입락
	
	private static void increment(){
        lock.lock(); //락획득

        try{
              for(int i=0;i<10000;i++) counter++;

        }finally{ //for문이 전부 실행되야 lock이 풀림. 즉, 동시진입이 안된다.
                lock.unlock();
        }	
}

	public static void main(String[] args){
	
		Thread t1 = new Thread(new Runnable(){
				@Override
				public void run(){
					increment();
				}
		});


		Thread t2 = new Thread(new Runnable(){
					@Override
					public void run(){
							increment();
					}
			});

			//두 개의 스레드 실행 		
			t1.start();
			t2.start();

			//호출된 스레드가 종료될때 까지 대기시킴
			t1.join();
			t2.join();

	}
}

스레드 두개가 동시 접근하여도 try-finally 블록을 이용하여 for문이 전부 실행될 경우에만 락을 돌려주도록 설정하였다. 

 

세마포어와 다중 버퍼 처리 

만약 4KB 버퍼를 1KB로 나누어서 네 개의 1KB 버퍼를 구성하면, 세마포어는 이 네 개의 버퍼에 연결될 수 있다. 이를 통해 사용자와 생성자는 서로 다른 버퍼에서 동시에 작업할 수 있다. 세마포어는 공유 자원(버퍼)에 대한 접근을 제어하고 사용자 및 생성자 간의 조정을 용이하게 사용하도록 설정해준다.

뮤텍스의 버퍼 처리

Mutex는 상호배제를 담당한다. 즉 생산자 또는 소비자 중 하나가 뮤텍스 락을 소유하고 작업을 계속할 수 있는 상황이다. 생산자가 버퍼를 채우는 동안 사용자는 기다려야 하며, 그 반대의 경우도 마찬가지이다. 뮤텍스 락이 설정된 경우 항상 단일스레드만 전체 버퍼에서 작업할 수 있다.

 

뮤텍스와 세마포어의 주요 차이점

  Semaphore 세마포어 Mutex 뮤텍스 
메커니즘 신호 메커니즘(signal mechanism) 잠금 메커니즘 (lock mechanism)
데이터타입 정수형 변수 객체
자원관리 만약 자원의 사용이 불가하면, 프로세스는 대기작업을 시행하고 세마포어의 카운터가 0 이상이 될 때까지 기다려야한다 뮤텍스가 잠겨 있는 경우 프로세스는 대기열에서 대기하게 된다
스레드 멀티 스레드를 가질 수 있다.  멀티 스레드를 가질 수 있지만 동시 접근이 불가하다. 
작업방법 세마포어의 값은 acquire()과 release()작업을 사용하여 변경한다. 잠금 또는 잠금해제로 사용된다.

 

세마포어의 장단점 

장점

  • 여러 스레드가 임계 구역에 동시에 '접근'할 수 있다.
  • 여러 프로세스가 임계 구역에 동시에 '진입'하는 것을 허용하지 않는다.
  • 리소스를 유연하게 관리할 수 있다. 
  • producer-consumer 문제(생산자-소비자 문제)를 해결할 수 있다.  

단점

  • 운영체제는 세마포어 신호 호출을 모두 추적해야한다.
  • 세마포어의 데드락을 피하기 위해 acquire()과 release() 작업은 올바른 순서로 설계되어야한다.
  • 세마포어의 프로그래밍은 복잡한 메소드이기 때문에 대규모로 사용하기에는 실용적이지 않다.
  • 프로그래밍 오류에 민감하며, 데드락 및 상호배제 위반이 발생할 가능성이 있다. 

뮤텍스의 장단점 

장점

  • 간단하게 잠금을 설정할 수 있으며, 임계 구역에 진입하기 전에 얻어진다. 
  • 항상 한번에 하나의 스레드만이 임계 구역에 있기 때문에 Race Condition이 발생하지 않으며 데이터는 일관성을 유지한다.

단점 

  • 동시에 과도한 스레드가 뮤텍스를 얻으려고 할 떄 CPU 자원이 낭비된다
  •  설계가 잘못된 경우 데드락에 빠질 수 있다. 

 

 

 

 

ETC.

  • 보통 OS 교과서에서 세마포어 메소드는 wait&signal로 써져있다. acquire&release는 java.util.concurrent.Semaphore에 정의된 wait과 signal의 메서드명이다. 
  • Binary Semaphore는 S가 0과 1밖에 없기 때문에 뮤텍스 락처럼 사용되기도 한다. 

 

 


 

 

 

참고 

 

https://www.geeksforgeeks.org/mutex-vs-semaphore/

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Semaphore.html

https://www.slideshare.net/mohammedarif89/semaphores

https://lordofkangs.tistory.com/27