이전글에 이어서 이번 글에서는 동시성 제어를 하는 기법에 대해 정리하였다.
Critical Section Problem을 방지하면서도 동시성을 제어하기 위해 사용하는 기법에 대해 알아보자!
다른 스레드가 공유 자원을 사용하지 못하도록 제한을 거는 대표적인 제어 기법
락은 Mutual Exclusion(상호배제)를 구현하기 위해 사용되는 기법이다.
한 번에 하나의 스레드만이 Critical Section(임계영역)에 접근할 수 있게 한다.
이를 통해서 잘 설계된 lock은 Deadlock과 Racecondition을 방지할 수 있다.
다양한 종류의 락이 있는데 OS 입장에서의 Lock에 대해서 정리하고자 작성하였기 때문에 이번글에서는 Spinlock과 Mutex에 대해서만 설명한다.
📢 애플리케이션 수준에서의 Lock은 다음글에서 직접 실습을 통해 기록할 예정
(비관적 락, 낙관적 락, 분산락 등등..)
락을 가지기 위해 계속해서 시도한다.
만약에 임계영역에 락이 걸려있어서 스레드가 진입할 수 없는 상황일 때 임계영역에 락이 풀렸다는 것을 어떻게 알 수 있을까?
그때 Spinlock은 진입이 가능할 때 까지 반복적으로 재시도 하는 방식을 사용한다.
이를 Busy waiting이라고 한다.
자바코드로 아주 간단하게 표현해보면 이런 느낌이다.
public class SpinLock {
private volatile boolean isLocked = false;
public void lock() {
while (true) {
// 락이 걸려있지 않다면, 락을 걸고 루프를 빠져나옵니다.
if (!isLocked) {
isLocked = true;
break;
}
// 락이 걸려 있다면, 계속해서 락을 시도합니다.
}
}
public void unlock() {
isLocked = false;
}
}
(이 코드는 실제로는 멀티스레드 환경에서 안전하지 않습니다 실제로는 atomic
을 사용하여야 함.)
Busy waiting
Spinlock이 기다리는 동안 CPU를 낭비한다는 단점이 있기 때문에 이를 보완하기 위해 나온것이 Mutex이다.
락을 가질 수 있을 때 까지 휴식을 한다. 락을 획득한 스레드만이 락을 해제할 수 있다.
Mutex는 상태가 오직 획득(Lock) / 해제(Unlock)만 존재한다는 점은 스핀락과 동일하다.
하지만 뮤텍스는 Sleep 상태로 들어갔다 락이 준비되면 Wakeup 해서 다시 권한 획득을 시도한다.
(
자바 코드로 간단하게 살펴보면 이런 느낌이다.
public class Mutex {
private boolean isLocked = false;
private int guard = 0;
public synchronized void lock() throws InterruptedException {
// 이미 락이 걸려 있다면, 락이 해제되고 guard를 획득할 때 까지 대기합니다.
while (isLocked && acquire(guard)) {
guard = 0;
wait();
}
// 락을 걸고, isLocked 상태를 true로 설정합니다.
isLocked = true;
guard = 0;
}
public synchronized void unlock() {
// 락을 해제하고, isLocked 상태를 false로 설정합니다.
isLocked = false;
// 대기중인 다른 스레드가 있다면깨웁니다.
notify();
guard = 0;
}
}
꼭 그렇지만은 않다.
Mutex 에서는 잠들고 깨는 과정에서 context switching이 일어나기 때문이다. (Spinlock에서는 context switching이 일어나지 않음)
따라서 만약 멀티코어 환경에서 critical section에서의 작업이 context switching 보다 빨리 끝난다면 spinlock이 뮤텍스보다 더 이점이 있다.
여기서 왜 멀티코어 환경이냐면 싱글코어에서 스핀락을 사용하면, 대기중인 스레드가 cpu를 독점하여서 다른 어떤 작업도 수행할 수 없기 때문이다.
한마디로 락을 획득하기 위한 대기시간 < context switching 시간 일때 spinlock이 더 유리할 수 있다.
리소스에 접근할 수 있는 허용 가능한 최대 스레드 수를 제어하는 데 사용
Semaphore와 Mutex는 매커니즘이 나름 유사하기 때문에 차이점 위주로 살펴보자
우선 세마포어에는 주로 두가지 유형이 있다.
세마포어가 0 또는 1의 값을 가질 수 있으며, 뮤텍스와 유사하게 작동한다.
바이너리 세마포어는 주로 상호 배제를 위해 사용된다.
정해진 수의 스레드만이 동시에 리소스에 접근할 수 있게 한다.
예를 들어, 세마포어의 값이 5라면, 최대 5개의 스레드가 동시에 리소스에 접근할 수 있다.
세마포어의 동작 방식은 이런식으로 이루어진다.
세마포어는 acquire()
메서드로 락을 획득하려고 시도하고, release()
메서드로 락을 해제한다.
acquire()
호출 시 세마포어의 카운트가 감소하고, release()
호출 시 카운트가 증가한다.
상호 배제만 필요하다면 Mutex를
작업 간의 실행 순서 동기화가 필요하다면 Semaphore를 쓰자!
자바에서는 synchronized
키워드를 통해 스레드간 동기화를 할 수 있다.
뮤텍스와 세마포어는 운영체제 수준에서 사용하는 동기화 메커니즘이라면 모니터는 주로 언어 레벨에서 지원된다.