스핀락의 가장 큰 문제는 바쁜 대기였다. 고수준 도구는 이를 블로킹으로 해결한다. 이 블로킹 위에 뮤텍스, 세마포, 모니터를 올린다.
실제 구현은 하이브리드 형식으로, 도구 내부의 짧은 자료구조 보호에는 스핀락을 쓰고, 실제 대기는 블로킹으로 처리한다.
한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 락을 걸어 소유하는 기법
프로세스가 임계구역에 들어갈 때 임계구역 진입을 잠근다.
다른 프로세스가 임계구역에 들어가고 싶으면 공유 자원이 잠겨있는지 확인하고 기다린다.
이 과정을 구현하기 위해서는 세 가지가 필요하다.
lock acquire release lock = False
def acquire(): # 바쁜 대기
while lock: # 확인
pass
lock = True # 잠금
def release():
lock = False
acquire() # 확인 및 잠금
-- 임계구역 코드 --
release() # 열기
acquire 함수에서, 확인과 잠금 사이에 틈이 있어서 동시성 이슈가 발생할 수 있다.
이를 방지하기 위해 아래처럼 TAS 원자적 연산으로 구현할 수 있다.
def acquire():
while test_and_set(lock): # 확인과 잠금이 원자적
pass
acquire 에서 바쁜 대기의 비효율성을 해결하기 위해, 프로세스 자체를 대기큐로 보낸다.
그리고 release 함수가 호출되면 대기중인 프로세스를 준비큐로 옮겨준다.
lock = False
queue = deque() # 대기 중인 스레드를 담을 큐
def acquire():
if lock: # 이미 잠겨 있으면
queue.append(process) # 큐에 본인을 넣고
sleep() # 재움
lock = True # 잠금
def release():
if queue: # 기다리는 스레드가 있으면
p = queue.popleft()
wakeup(p) # 하나 깨움 → 걔가 이어서 잠금
else:
lock = False # 없으면 잠금 해제
여러 개의 스레드가 동시에 공유 자원에 접근할 수 있도록 허용하며, 접근 가능한 자원의 개수를 카운팅 하여 제어하는 신호 메커니즘
임계 구역 앞에서 멈춤 신호를 받으면 잠시 기다린다.
통과 신호를 받으면 임계 구역에 들어간다.
공유 자원이 여러 개 있을 때에도, 사용 가능한 공유 자원이 하나라도 있으면 임계구역을 통과할 수 있다.
이 과정을 구현하기 위해서는 세 가지가 필요하다.
S wait() signal() S = 3
def wait():
S -= 1
if S < 0:
queue.append(process)
sleep() # 현재 process 를 대기 상태로 바꾸기
def signal():
S += 1
if S <= 0: # 대기 중인 프로세스가 있으면
process = queue.popleft()
wakeup(process) # 꺼낸 process 를 준비 상태로 바꾸기
wait()
-- 임계구역 코드 --
signal()
wait 함수는 프로세스를 대기큐로 보낸다. signal 함수를 호출하면, 대기중인 프로세스를 준비큐로 옮겨준다. 세마포어는 뮤텍스와 다르게, 락을 걸지 않은 다른 스레드도 Signal을 보내 해제할 수 있다.
S = Semaphore(0) # 초기값 0으로 시작
# 스레드 A — 데이터 준비 담당
def thread_A():
prepare_data() # 오래 걸리는 준비 작업
S.signal() # 준비 끝났다 신호 ← A는 wait()를 부른 적이 없음
# 스레드 B — 데이터 사용 담당
def thread_B():
S.wait() # A가 끝날 때까지 대기 ← B는 signal()을 부른 적이 없음
use_data() # A가 준비한 데이터 사용
세마포어를 통해 상호 배제 뿐만 아니라 실행 순서도 제어할 수 있다.
나중실행프로세스 는 임계구역 진입 전 wait 함수를 만나서 대기 상태로 들어가기 때문에, 먼저실행프로세스 가 임계구역에 먼저 진입하도록 제어된다.
S = Semaphore(0)
def 먼저실행프로세스():
# 임계 구역 #
S.signal()
def 나중실행프로세스():
S.wait()
# 임계 구역 #
| 구분 | 뮤텍스 | 세마포어 |
|---|---|---|
| 동기화 대상 개수 | 오직 1개 (이진 락) | 1개 이상 (카운팅 가능) |
| 상태 소유권 | 락을 건 스레드만 락을 해제할 수 있음 (소유주 책임) | 락을 걸지 않은 다른 스레드도 Signal을 보내 해제할 수 있음 |
| 주요 목적 | 상호 배제 - 공유 자원 독점 보호 | 자원 관리 및 실행 순서 제어 - 허용된 개수만큼 접근 제한 |
생산자-소비자처럼 A가 wait하고 B가 signal하는 구조에서는 소유권이 방해되므로 세마포가 적절하다.
반대로 락을 쥔 흐름을 알아야 하는 우선순위 상속은 뮤텍스만 가능하다.
임계구역마다 일일이 wait , signal 함수를 쓰는게 번거로운데, 모니터를 통해 이 과정을 자동으로 처리할 수 있다.
공유 자원을 숨기고, 공유 자원에 접근하기 위한 통로를 묶어서 인터페이스로 관리한다. 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근할 수 있다.
프로세스가 모니터에 작업을 요청하면 모니터는 이 작업을 큐에 저장한 후 순서대로 작업을 처리하고, 그 결과만 프로세스에게 반환한다. 이를 통해 상호 배제 동기화를 제공한다.
모니터는 실행 순서 제어 동기화도 제공하는데, 여기에는 조건 변수를 사용한다.
조건 변수는 프로세스가 모니터에 진입할 수 있는 조건을 만족할 때까지 들어오지 못하게 대기시키는 역할을 하는데, 조건 변수마다 큐를 가지고 있다.
조건 변수 x 에 대해 wait 를 호출 큐 x에 프로세스B가 추가되고 실행이 대기됨x 에 대해 signal 을 호출 