CS

[운영체제] 동시성 이슈 해결 방법 - Semaphore/Mutex

nkdev 2024. 12. 31. 06:31

지난 포스팅에서 동시성 문제인 '레이스 컨디션'과 '데이터 레이스'에 대해 알아보았다.

이번 포스팅에서는 그 해결 방법인 Semaphore과 Mutex에 대해 알아보자.

동시성 이슈

주요 원인 :

  • data race
    • 데이터 접근 절차는 atomic하지 않음 (읽기->연산->쓰기)
    • 단일 코어 : 하나의 instructor 실행 중 cpu 사용 권한을 뺏기는 경우 동시에 같은 memory address 읽을 가능성 존재
    • 멀티 코어 : 여러 cpu에서 동시에 같은 memory address 읽을 가능성 존재
  • race condition
    • 멀티 스레드 : 프로세스의 공유 데이터 사용 중 발생
    • 멀티 프로세스 : 커널 내부의 공유 데이터 사용 중 발생

해결 방법 :

  • 단일 코어 : 공유 자원에 접근한 경우 계속해서 그 프로세스가 cpu를 점유하도록 는 방법 (context switching을 막음)
  • 멀티 코어 : 공유 자원에 접근한 경우 해당 자원에 다른 프로세스가 접근할 수 없게 lock을 거는 방법

 

Java에서 지원하는 동기화 기법

  1. 세마포어(Semaphore)
  2. 뮤텍스(Mutex)

세마포어(Semaphore)

  • N개의 스레드만 공유 자원에 접근 가능하도록 제어
  • N개의 허가증 -> 허가증을 가진 스레드만 접근 가능 / 남은 허가증 없으면 허가증이 반납될 때까지 대기
  • 카운팅 세마포어(Counting Semaphore)라고도 함 
  • java.util.concurrent.Semaphore

예제 코드

public class SemaphoreExample {
    private final Semaphore semaphore = new Semaphore(3); //동시에 3개의 스레드만 접근 가능

    public void accessResource(int threadId) { //해당 스레드가 자원에 접근할 수 있는지 확인 & 작업 수행 후 자원 반납하게 하는 메서드
        try {
            System.out.println("Thread " + threadId + " is trying to access the resource.");
            semaphore.acquire(); //자원 획득 시도
            
            System.out.println("Thread " + threadId + " is accessing the resource.");
            Thread.sleep(1000); //자원 획득 후 작업 수행 //지연 시간 추가
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            
        } finally {
            System.out.println("Thread " + threadId + " is releasing the resource.");
            semaphore.release(); //자원 반납
        }
    }
}

Semaphore.acquire() : 자원 획득 -> 허가증 개수(접근 가능한 스레드 개수)를 감소시킴

Semaphore.release() : 자원 반납 -> 허가증 개수(접근 가능한 스레드 개수)를 증가시킴

  • acquire()로 자원 획득 시도했으나 허가증이 남아있지 않다면 해당 스레드는 자발적으로 대기(blocked) 상태로 전환
  • 다른 스레드가 release()로 허가증을 반환하면서 대기 중이던 스레드를 깨우고 실행 대기 상태로 만듦
  • 허가증 하나가 release()되면 대기 중이던 스레드가 실행됨 

테스트 코드

class SemaphoreExampleTest {

    @Test
    @DisplayName("Semaphore로 자원접근")
    void semaphoreAccess() throws InterruptedException {
        final SemaphoreExample example = new SemaphoreExample();
        ExecutorService executor = Executors.newFixedThreadPool(10); //스레드 풀에 미리 병렬 작업할 10개의 스레드를 생성해둠

        for (int i = 0; i < 10; i++) {
            final int threadId = i;
            executor.submit(() -> example.accessResource(threadId)); //스레드에 task를 할당
        }

        executor.shutdown(); //모든 task 수행이 완료되면 executor service(스레드 풀)종료
        boolean finished = executor.awaitTermination(20, TimeUnit.SECONDS); //최대 20초간 스레드 작업이 완료되길 기다림
        assert finished; //작업 완료 여부 확인
    }
}

이미지 출처 : &nbsp;https://www.baeldung.com/thread-pool-java-and-guava

 

 

최대 3개의 스레드가 접근 가능

허가증을 반환해야 대기 중이던 스레드가 접근할 수 있다 

 

 

뮤텍스(Mutex)

  • 1개의 스레드만 공유 자원에 접근 가능하도록 제어 
  • 1개의 lock -> lock을 가진 스레드만 접근 가능 / lock이 이미 사용되고 있으면 lock이 반납될 때까지 대기
  • 이진 세마포어(Binary Semaphore)
  • java.util.concurrent.locks.ReentrantLock

 

예제 코드

public class MutexExample {
    private final Lock lock = new ReentrantLock();

    public void accessResource(int threadId) { //해당 스레드가 자원에 접근할 수 있는지 확인 & 작업 수행 후 자원 반납하게 하는 메서드
        System.out.println("Thread " + threadId + " is trying to access the resource.");
        lock.lock(); //자원 획득 시도
        
        try {
            System.out.println("Thread " + threadId + " is accessing the resource.");
            Thread.sleep(1000); //자원 획득 후 작업 수행 //지연 시간 추가
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            
        } finally {
            System.out.println("Thread " + threadId + " is releasing the resource.");
            lock.unlock(); //자원 반납
        }
    }
}

Lock.lock(): 자원 획득 -> lock hold count를 1로 변경

Lock.unlock() : 자원 반납 -> lock hold count를 0으로 변경 

  • lock()으로 접근 시도했으나 이미 다른 스레드가 사용 중이라면 해당 스레드는 대기(blocked) 상태로 전환
  • 현재 자원 점유 중인 스레드가 unlock()할 때까지 대기

테스트 코드

class MutexExampleTest {
    @Test
    @DisplayName("Mutex로 자원접근")
    void mutexAccess() throws InterruptedException {
        final MutexExample example = new MutexExample();
        ExecutorService executor = Executors.newFixedThreadPool(5); //스레드 풀에 미리 병렬 작업할 5개의 스레드를 생성해둠

        for (int i = 0; i < 5; i++) {
            final int threadId = i;
            executor.submit(() -> example.accessResource(threadId)); //스레드에 task를 할당 
        }

        executor.shutdown(); //모든 task 수행이 완료되면 executor service(스레드 풀) 종료
        boolean finished = executor.awaitTermination(10, TimeUnit.SECONDS); //최대 10초간 스레드 작업이 완료되길 기다림
        assert finished; //작업 완료 여부 확인
    }
}

최대 1개의 스레드가 접근 가능

스레드가 lock을 반환해야 다른 스레드가 접근할 수 있음 

 

Mutex의 문제점

  • Deadlock
    • 두 스레드 모두 R1, R2가 필요
    • 스레드1이 먼저 R1에 lock을 걸고 자원을 점유
    • 뒤이어 스레드2가 R2에 lock을 걸고 자원을 점유
    • 스레드 1,2는 서로가 가진 자원을 얻기 위해 무한 대기

 

-> 이 문제를 해결하기 위해서는 스레드가 자원을 점유하는 순서를 정하든가(스레드1, 2 모두 p1을 첫 번째로, p2를 두 번째로 점유해야만 하도록 만들면 deadlock발생 위험을 줄일 수 있음), 스레드가 lock을 갖고 있는 시간에 타임 아웃을 걸면 된다.

 

Semaphore와 Mutex 비교

  Semaphore Mutex
동작 방식 하나의 자원에 여러 스레드가 동시에 접근 하나의 자원에 한 개의 스레드만 접근
자원의 소유권 소유권이 존재하지 않음 lock을 획득한 스레드가 자원을 소유
사용 목적 리소스의 접근 횟수를 제한하기 위해 데이터 무결성 보장하기 위해
사용 예시 API 호출 제한 
DB 커넥팅 풀 
파일 쓰기 작업 
DB 트랜잭션 
Java에서 제공하는 메서드 aquire()
release()
lock()
unlock()
문제점 설계 및 디버깅이 복잡하다 Deadlock

세마포어는 주로 네트워크나 DB의 최대 연결 횟수를 제한하여 서버 과부하를 방지할 때 사용됨

뮤텍스는 주로 트랜잭션이나 파일의 쓰기 작업처럼 데이터를 동시에 읽고 쓰면 안 되는 경우에 사용됨

 


 

* reference

 

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

 

ExecutorService (Java Platform SE 8 )

An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks. An ExecutorService can be shut down, which will cause it to reject new tasks. Two different methods are p

docs.oracle.com

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

 

Semaphore (Java Platform SE 8 )

Releases the given number of permits, returning them to the semaphore. Releases the given number of permits, increasing the number of available permits by that amount. If any threads are trying to acquire permits, then one is selected and given the permits

docs.oracle.com

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html

 

ReentrantLock (Java Platform SE 7 )

Acquires the lock only if it is not held by another thread at the time of invocation. Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to

docs.oracle.com

https://notavoid.tistory.com/31

 

뮤텍스와 세마포어: 자바에서의 동시성 제어 이해하기 (WITH JAVA, JUNIT5)

멀티스레딩 환경에서 여러 스레드가 동시에 같은 리소스에 접근하려 할 때, 데이터의 일관성과 무결성을 유지하는 것이 중요합니다. 이를 위해 자바에서는 뮤텍스(Mutex)와 세마포어(Semaphore) 같

notavoid.tistory.com

https://heeonii.tistory.com/14

 

[운영체제] Mutex 뮤텍스와 Semaphore 세마포어의 차이

프로세스 간 메시지를 전송하거나, 공유메모리를 통해 공유된 자원에 여러 개의 프로세스가 동시에 접근하면 Critical Section(여러 프로세스가 데이터를 공유하며 수행될 때, 각 프로세스에서 공유

heeonii.tistory.com

https://hoestory.tistory.com/84

 

[Java] 동기화 기법에 대하여(뮤텍스, 세마포어) - 1

들어가기 전동기화를 하지 않으면 동시성 이슈로 인해 예상치 못한 문제를 겪을 수 있어 동기화를 하여 이와 같은 문제를 방지해야 합니다. 그래서 이번 포스팅에서는 동시성 이슈를 방지하기

hoestory.tistory.com