Post

스핀락, 뮤텍스, 세마포에 대해

우리는 멀티스레드 환경에서 race condition(경쟁 조건)이 발생하지 않도록 동기화(synchronization)를 해줘야한다.

하나의 프로세스/스레드만 진입해서 실행한다는 것인 mutal exclusion을 어떻게 보장해 critical section(임계 영역)으로 만들 수 있을까?

그 방법에는 세가지 방법이 있다.

스핀락(spinlock)

스핀락이란 멀티 스레드 환경에서 하나의 스레드만 lock에서 탈출해 그 스레드만 critical section으로 진입하게 하는 방식이다.

그럼 어떻게 lock을 걸까?

자바에서는 다음과 같은 방법이 있다.

AtomicBoolean을 활용한 스핀락 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 락을 얻을 때까지 반복
            Thread.yield(); // 다른 스레드에게 CPU 양보
        }
    }

    public void unlock() {
        locked.set(false); // 락 해제
    }
}

이 스핀락은 AtomicBooleancompareAndSet 메서드를 사용하여 락을 획득하고 해제한다. lock 메서드에서는 compareAndSetfalse인 경우에만 true로 설정하여 락을 획득하고, 이미 락이 획득된 경우에는 계속 반복하면서 대기한다. unlock 메서드에서는 락을 해제하기 위해 AtomicBoolean의 값을 false로 설정한다.

근데 위의 코드를 보면 의문점이 하나 생길거다.

락을 얻을 때까지 반복하는 건 알겠는데, 저 compareAndSet을 실행 중일 때 C.S가 발생해 여러 스레드가 lock을 획득하는거 아닌가??

하지만 실제로는 그렇지 않다.

그 이유는 atomic이라는 용어 때문인데, atomic은 CPU에서 관리하는 명령어로 다음과 같은 특징이 있다.

atomic

  • 실행 중간에 간섭받거나 중단되지 않는다.
  • 같은 메모리 영역에 대해 동시에 실행되지 않는다.

이런 특성 때문에 우리는 mutal exclusion을 보장할 수 있는 것이다.

하지만 이런 스핀락에는 큰 단점이 있다. 스핀락은 락을 획득하기 전까지 계속 스핀(반복)을 하기 때문에 이런 작업이 CPU를 많이 잡아먹는다. 즉, Busy-Waiting으로 CPU 오버헤드가 늘어나는 단점이 있다.

하지만 CPU를 많이 잡아먹을 가능성이 있다고 무조건 스핀락 사용을 지양해야하는 것은 아니다.

만약 멀티코어 환경이고, 스핀락 중 발생하는 Busy-Waiting으로 인해 발생할 CPU 오버헤드보다 critical section의 작업이 더 빨리 끝난다면 스핀락을 사용하는 것을 고려해볼 수 있다. 하지만 서비스의 안정성을 위해 많이 사용하지는 않는다.

뮤텍스(mutex)

뮤텍스란 무엇일까?

스핀락의 경우 스레드가 critical section에 들어가기 전까지 계속해서 반복한다고 했다.

하지만 뮤텍스의 경우 계속 반복하지 않고 lock을 획득하지 못한 상태라면 그 스레드를 잠들게 한다.

여기서 잠들게 한다는 것은 어떤 의미일까?

예를 들어 lock을 획득하지 못한 스레드를 Queue에 넣어 둔 뒤 lock을 획득한 스레드가 critical section에 실행을 끝내고 unlock했을 때 Queue에 담아둔 스레드를 불러오는 등의 방법이 있다.

아래는 자바에서 뮤텍스를 구현한 코드이다. 자바에서는 Queue를 사용하기보단 ReentrantLock을 사용해 스레드를 제어한다. (자세한 사용법은 다음에 알아보자.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexExample {

    private final Lock mutex = new ReentrantLock();

    public void criticalSection() {
        try {
            mutex.lock(); // 락 획득
            // 임계 영역: 공유 자원에 안전하게 접근
            System.out.println("Thread " + Thread.currentThread().getId() + " in critical section");
        } finally {
            mutex.unlock(); // 락 해제
        }
    }

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

        // 여러 스레드가 공유 자원에 접근
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                example.criticalSection();
            }).start();
        }
    }
}

보통 synchronized 키워드를 사용하지만 ReentrantLock을 사용할 경우 특수한 동기화 상황에 대응할 수 있고, 더 세밀한 제어가 가능해진다고 한다.

뮤텍스의 주요 특징들을 정리해보자.

mutex

  • mutex는 한 번에 하나의 스레드만이 접근할 수 있도록 보장.
  • Lock을 가진 스레드만이 unLock 가능.
  • Lock을 사용해 Busy-Waiting이 적다.

이 외에도 다양한 장점이 있다.

세마포(semaphore)

세마포란 signal mechanism을 가진, 하나 이상의 프로세스/스레드가 critical section에 접근 가하도록 하는 장치이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.Semaphore;

public class ShortSemaphore {

    public static void main(String[] args) {
        // 세마포어 생성, 초기 허용 허가 수: 2
        Semaphore semaphore = new Semaphore(2);

        // 스레드가 수행할 작업 정의
        Runnable task = () -> {
            try {
                semaphore.acquire(); // 세마포어 획득, 허용 허가 수 감소
                System.out.println("Thread " + Thread.currentThread().getId() + " acquired the semaphore.");
                Thread.sleep(2000); // 공유 리소스에 대한 작업 수행 (2초 동안 대기)
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); // 세마포어 반환, 허용 허가 수 증가
                System.out.println("Thread " + Thread.currentThread().getId() + " released the semaphore.");
            }
        };

        // 여러 스레드가 세마포어를 공유하여 사용
        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}

이는 자바에서 간단히 세마포를 구현한 코드이다.

뮤텍스와 세마포의 동작 방식은 거의 비슷하다. 하지만 큰 차이가 있다.

그것은 바로 스레드 허용 허가 수가 다르다. 즉, 뮤텍스는 1개의 스레드만 작업하도록 했다면 세마포는 하나 이상의 스레드를 보장한다.

1
Semaphore semaphore = new Semaphore(2);

이 부분을 살펴보면 Semaphore에 (2) 정수를 넣어준 것을 알 수 있다. 이 수가 바로 동시에 실행가능하게 할 허용의 수이다.

하나의 스레드가 들어가면 허용 허가 수를 줄이고 반대로 작업을 마치고 빠져나온다면 허용 허가 수를 증가 시켜 하나 이상의 스레드가 동작하게 한 것이다.

이진 세마포와 뮤텍스의 차이

뮤텍스와 세마포는 거의 유사한 특성을 가지고 있다고 했다.

그럼 1개의 스레드만 작업하게 하는 세마포와 뮤텍스는 같은 것이 아닌가??

답은 아니다.

1. 세마포는 순서를 정할 수 있다.

세마포의 중요한 특성이 있는데 세마포는 순서를 정해줄 때 사용이 가능하다.

그게 무슨 의미일까?

뮤텍스의 경우 lock을 획득한 자만이 unlock을 할 수 있다는 특성이 있다고 했다.

하지만 세마포의 경우에는 그렇지 않다.

위에서 세마포는 signal mechanism을 가진다고 했다. 그 의미가 여기서 나온다.

만약 그림과 같이 두개의 프로세스가 있다고 가정해보자.

스크린샷 2024-01-25 012943

위의 두 개의 프로세스는 각각 signal()과 wait()을 가지고 있다. 그럴 때 두 프로세스가 실행된다고 가정해보자.

그 때 task1이 먼저 실행될지, task2가 먼저 실행될지는 모르지만 두 가지 경우 모두 task3가 실행되기 위해서는 task1이 실행되어야 한다. 다음과 같다.

1
2
3
4
5
if task1 먼저 실행
- task1 실행 -> signal() 날려줌 -> task2 -> wait() -> wait() 해제 -> task3 실행

if task2 먼저 실행
- task2 실행 -> wait() -> task1 실행 -> signal() 날려줌 -> wait() 해제 -> task3 실행

2. 세마포와 달리 뮤텍스는 priority inheritance 속성을 가짐

priority inheritance는 무엇일까?

우리들의 사용하는 운영체제에는 우선순위라는 것이 존재한다. 어떤 프레세스가 더 빨리 실행되어야 하는지를 정해주는 것인다. 이때 이것을 CPU 스케줄링이라고 부른다.

만약 두 개의 프로세스 P1과 P2이 있다고 가정해보자. P1은 높은 우선순위를 가지고, P2는 P1에 비해 낮은 우선순위를 가지고 있다.

그때 만약 두 프로세스가 Lock에 동시에 접근해 P2가 Lock을 획득해 critical section에 들어갔을 때 문제가 발생한다.

우선순위가 높은 P1이 더 낮은 우선순위를 가진 P2가 작업을 끝낼 때까지 실행되지 못하고 대기하게 된다.

그때 필요한게 priority inheritance이다.

priority inheritance은 만약 하나의 Lock에 접근해 위와 같은 상황이 되었을 때, 우선순위가 낮은 P2의 우선순위를 P1과 같은 우선순위 일시적으로 올려준다.

그로인해 더 빨리 처리되어 우선순위가 높은 P1이 처리될 수 있는 효과를 볼 수 있다.

뮤텍스는 이런 priority inheritance 속성을 가지지만 세마포는 누가 Lock을 가질지, signal은 누가 보낼지 알 수 없기 때문에 이러한 속성을 가지지 못한다.

####

정리

스핀락과 뮤텍스, 세마포에 대해 알아보았다.

상호 배제만 필요하다면 뮤텍스를 사용

작업 간의 실행 순서 동기화가 필요하다면 세마포를 권장한다고 한다.

스핀락, 뮤텍스, 세마포의 구체적인 동작 방식은 OS와 프로그래밍 언어에 따라 조금씩 다르기 때문에 관련 문서를 잘 읽어보자….

출처)

스핀락(spinlock) 뮤텍스(mutex) 세마포(semaphore) 각각의 특징과 차이 완벽 설명! 뮤텍스는 바이너리 세마포가 아니라는 것도 설명합니다! (youtube.com)

ChatGPT (openai.com)

This post is licensed under CC BY 4.0 by the author.