[운영체제] 프로세스 동기화 기법 (고수준)

Local Gaji·2023년 12월 29일

운영체제

목록 보기
10/19

바쁜 대기 vs 블로킹

스핀락의 가장 큰 문제는 바쁜 대기였다. 고수준 도구는 이를 블로킹으로 해결한다. 이 블로킹 위에 뮤텍스, 세마포, 모니터를 올린다.

  • 바쁜 대기: 조건이 될 때까지 루프를 돈다
  • 블로킹: 대기하는 흐름을 대기 큐로 보내고 (sleep) CPU를 양보한다. 조건이 충족되면 OS가 깨운다 (wakeup).

실제 구현은 하이브리드 형식으로, 도구 내부의 짧은 자료구조 보호에는 스핀락을 쓰고, 실제 대기는 블로킹으로 처리한다.


뮤텍스 락

한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 락을 걸어 소유하는 기법

프로세스가 임계구역에 들어갈 때 임계구역 진입을 잠근다.
다른 프로세스가 임계구역에 들어가고 싶으면 공유 자원이 잠겨있는지 확인하고 기다린다.

이 과정을 구현하기 위해서는 세 가지가 필요하다.

  • 전역변수 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 함수를 호출하면, 대기중인 프로세스를 준비큐로 옮겨준다.

🎈 뮤텍스 vs 세마포어

세마포어는 뮤텍스와 다르게, 락을 걸지 않은 다른 스레드도 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 함수를 쓰는게 번거로운데, 모니터를 통해 이 과정을 자동으로 처리할 수 있다.

공유 자원을 숨기고, 공유 자원에 접근하기 위한 통로를 묶어서 인터페이스로 관리한다. 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근할 수 있다.

프로세스가 모니터에 작업을 요청하면 모니터는 이 작업을 큐에 저장한 후 순서대로 작업을 처리하고, 그 결과만 프로세스에게 반환한다. 이를 통해 상호 배제 동기화를 제공한다.

모니터는 실행 순서 제어 동기화도 제공하는데, 여기에는 조건 변수를 사용한다.
조건 변수는 프로세스가 모니터에 진입할 수 있는 조건을 만족할 때까지 들어오지 못하게 대기시키는 역할을 하는데, 조건 변수마다 큐를 가지고 있다.

  1. 모니터에 진입한 프로세스B가 조건 변수 x 에 대해 wait 를 호출
  2. 큐 x에 프로세스B가 추가되고 실행이 대기됨
  3. 이후 프로세스A가 x 에 대해 signal 을 호출
  4. 프로세스B가 큐에서 삭제, 대기 상태에서 벗어나 모니터로 진입

0개의 댓글