[F-Lab 모각코 챌린지 40일차] 뮤텍스, 세마포어

부추·2023년 7월 10일
0

F-Lab 모각코 챌린지

목록 보기
40/66

TIL

  1. spinlock
  2. 뮤텍스
  3. 세마포어



OS에서 여러 스레드가 공유하는 공유 자원에 대한 동시성 문제를 해결하기 위해 프로그램적으로 사용하는 여러 방안들을 이론적으로 살펴보고, 해당 방법들을 간단한 자바 코드로 작성해보겠다. 그 전에 간단한 용어 소개부터.

  • 공유 자원 : 여러 스레드 / 프로세스가 공유하는 자원.
  • 임계 영역 : 스레드 / 프로세스가 공유 자원에 접근하는 영역.

자바 중심으로 살펴볼 것이기 때문에 실행 단위를 간단하게 스레드로 좁히겠다. 공유 자원에 대해 동시성 문제를 컨트롤하기 위해, 스레드는 임계 영역에 들어가기 전 '락(lock)'을 얻어야 한다. CS에서 동시성 컨트롤은 이 '락 컨트롤'이라 생각해도 무방하다. 락 컨트롤에는 락과 관련해서, 어떻게 스레드들이 락을 얻고, 락을 내려놓고, 어떤 스레드에게 락을 할당할지 결정하고 구성하는 과정들이 포함된다.



1. spinlock(스핀락)

생각할 수 있는 가장 간단한 방법이다. 서로 다른 스레드 t1, t2가 임계 영역에 진입하는 과정이다. t1이 먼저 락을 획득하는 상황을 가정하겠다.

  1. t1이 락을 획득하여 임계 영역에 진입
  2. t2 역시 진입하려하지만, t1이 락을 획득했기 때문에 불가능
  3. t2는 계속 반복문을 돌며 락의 반환 여부를 확인. 이 과정을 스핀락 이라고 함.
  4. t1 작업이 끝나고 락을 반환
  5. t2는 락이 반환된 것을 확인, 자신이 락을 가지고 임계 영역에 진입

요컨데, 먼저 락을 쥐고 임계 영역에 진입한 스레드가 락을 내려놓을 때까지 계속 루프를 도는 과정이다. 해당 과정을 자바 코드로 작성했다. 'volatile'는 스레드 캐시로 인한 가시성 문제 때문에 붙인 키워드인데, 자세한 것은 포스트를 참고.

public class SpinlockDriver {
    private static volatile boolean lock = false;
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (lock) {
                Thread.onSpinWait();
            }

            lock = true;
            System.out.println("thread1 : 락을 획득했숑.");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock = false;
        });

        Thread thread2 = new Thread(() -> {
            while (lock) {
                Thread.onSpinWait();
            }

            lock = true;
            System.out.println("thread2 : 락을 획득했숑.");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock = false;
        });

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

lock boolean값이 true일 때 어떤 스레드가 lock을 획득한 상태고, false일 때 그 반대의 상황이다. thread1과 thread2 모두 lock이 false여야만 락을 획득하고 sleep()과정을 진행한다.

# 언제 스핀락이 괜찮을까?

while문을 돌며 끊임없이 lock을 확인하는 것은 좋지 않다. 스핀락을 돌고있는 동안 다른 일을 할 수도 있는 CPU 자원 낭비도 있고, 같은 스레드간 Context Switching도 자주 일어나니 성능에도 좋지 않다. 그럼에도 불구하고 스핀락이 나쁘지 않은 선택일 때가 있는데,

1) 일단 멀티코어 환경에서다. spinlock이 좋지 않은 이유는 t1 임계영역의 작업을 빨리 끝내고 t2에게 락을 넘겨줘야하는 상황에서, t2가 자꾸만 귀찮게 락을 확인하느라 CPU 자원을 활용하기 때문에 t1의 작업까지 영향을 주기 때문이다. 이때 멀티코어 환경에서라면 t2의 spinlock 작업이 t1 task를 수행하는 코어와 다른 코어에서 일어나 딱히 t1을 방해하지 않기 때문에 괜찮은 선택이 될 수 있다.

2) 그리고 context switching 비용이 클 때이다. 락을 획득하기 위해서 이렇게 spinlock을 도는 것 이외에도 lock을 획득하고자 하는 스레드를 대기 큐에 넣는 방식도 있다. 락을 관리하고, 스레드 상태를 변화시키고, 큐에 집어넣고, 락을 놓았을 때 큐를 다시 확인하고, 큐에서 스레드를 빼고, 스레드 상태를 변화시키고 ... 하는 락 관련 대기 큐 작업들은 커널 모드에서 일어난다. context switching은 비용이 큰 작업이다. 만약 spinlock을 짧게 돌아 락을 획득할 수 있는 상황이라면 커널모드의 대기 큐 작업을 하는 것이 더 "굳이"인 상황이 될 수도 있다.



2. Mutex

MUTual EXclusion 단어의 앞글자를 딴 이름으로, 공유 자원의 동시 접근을 막기 위한 매커니즘이다. 1번의 Spinlock과 굳이 나눴지만 '뮤텍스 락을 획득한 뒤 임계 영역에 진입한다' 라는 락 기반 동작이라는 설명에 기반했을 때, 크게 다를 것은 없다. 다만 락을 획득하지 못한 스레드가 꼭 spinlock을 돌지 않고 대기 큐에 들어갈 수도 있다는 조금 더 일반적인 락이라는 점에서 스핀락 설명을 따로 뺐다.

방금 언급했듯, 뮤텍스 역시 락 방식으로 동작한다. 임계 영역에 진입하기 전의 프로세스가 락을 획득하고, 락을 획득하지 못한 스레드는 앞선 스레드가 락을 놓을때까지 대기하는 매커니즘이다. 대기의 방식에도 크게 2가지가 있는데, 하나가 앞서 설명한 스핀락 방식이고 다른 하나가 대기 큐 방식이다.

대기 큐는 말 그대로, 락을 획득하지 못해 락을 획득할 수 있을 때까지 스레드를 대기 큐에 넣는 방식이다. 앞선 스핀락 방식에서는 스레드가 직접 락을 획득할 수 있을 때까지 끊임없이 스핀을 돌지만, 대기 큐 방식에서는 큐에 잠들어있다가 OS가 "너한테 락을 줄게" 하고 깨웠을 때 다시 동작한다.



3. Semaphore

semaphore은 락이 아닌, 시그널 방식으로 동작하는 동기화 매커니즘이다.

mutex는 완전 상호 배제인 '하나'의 공유자원을 위한 매커니즘이다. 그러나 semaphore는 하나의 자원이 아니라, 여러 스레드가 지정된 숫자만큼 리소스에 동시에 액세스하는 것을 허용한다. 쉽게 말하면 락이 여러 개 있는 것이다.

스레드가 임계 영역에 들어가기 위해 wait() 메소드를 호출한다. wait()에서는 semaphore의 value값을 확인한다. value가 0 이하라면 wait 상태에 들어간다. 그렇지 않다면, value값을 1 감소시킨 뒤 임계 영역에 진입하여 로직을 수행한다. 그리고 임계 영역을 벗어날 때, signal()을 호출하는데 이는 semaphore의 value값을 1 증가시키고 만약 wait 상태에 있는 스레드가 있다면 그들을 깨우는 작업을 수행한다.

Semaphore의 value 값만큼 스레드들이 공유 자원에 접근할 수 있다는 점이 포인트다. value를 1로 두면 mutex처럼 동작시킬 수 있다.

..어? 그러면.

# Mutex와 이진 semaphore의 다른 점이 뭐죠?

이렇게만 설명했을 때, 이진 semaphore는 뮤텍스와 다른 점이 없어보이지만 확실한 차이는 있다.

  • Mutex는 락을 건 스레드만이 락을 해제할 수 있다.
  • Mutex는 priority inheritence 속성을 가진다.

1번의 차이는 이해가 간다. 2번을 설명하겠다!

스레드1과 스레드2는 각각 1,2의 우선순위를 가진다. 스레드 2가 먼저 락을 획득해서 임계 영역에 진입하면, 스레드1은 스레드2가 락을 놓을 때까지 대기해야한다. 이때, 스레드2의 우선순위가 낮아서 CPU 스케줄링 환경에서 빨리 실행되지 못한다면 우선순위가 높은 스레드1은 스레드2때문에 실행되지 못하는 상황이 발생할 수 있다. 이때문에 OS에선 스레드2의 우선순위를 스레드1과 같은 레벨로 올린다. 이것이 priority inheritence이다.



REFERENCE

https://www.youtube.com/watch?v=gTkvX2Awj6g&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=5

https://www.youtube.com/watch?v=oazGbhBCOfU

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글