본문 바로가기
Study/CS

03-3 동기화와 교착 상태

by dailycoding777 2025. 2. 12.

공유 자원

프로세스나 스레드가 공유하는 자원

공유자원은 메모리나 파일이 될 수도 있고 , 전역변수나 입출력장치가 될 수 있다.


임계 구역(Critial Section)

하나의 프로세스/스레드만 접근하도록 보호해야 하는 코드 영역

경쟁 상태(Race Condition) 를 방지하기 위해 사용된다.

  • 경쟁 상태(Race Condition) : 여러 개의 스레드(또는 프로세스)가 공유 자원에 동시에 접근할 때, 실행 순서에 따라 예측하지 못한 결과가 발생하는 문제

💡 예제 (파이썬 멀티스레드 환경)

import threading

counter = 0  # 공유 변수

def increment():
    global counter
    for _ in range(1000000):
        counter += 1  # 임계 구역

threads = []
for _ in range(5):  # 5개 스레드 실행
    t = threading.Thread(target=increment)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("최종 counter 값:", counter)  # 예상: 5000000, 실제로는 다르게 나올 수 있음 (Race Condition 발생)

💥 실행 결과

  • 기대값: 5000000
  • 하지만 실행할 때마다 다른 값이 나올 수 있음 → Race Condition 발생

📌 해결 방법 (임계 구역 보호)

1️⃣ 뮤텍스(Mutex) 사용 (Lock)

import threading

counter = 0
lock = threading.Lock()  # 🔒 뮤텍스 생성

def increment():
    global counter
    for _ in range(1000000):
        with lock:  # 🔒 락을 걸어서 한 번에 한 스레드만 접근 가능
		                # 클래스의 객체(인스턴스) , 파이썬 제공
		                # with lock 안에 있는 코드가 임계구역
            counter += 1 

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("최종 counter 값:", counter)  # 항상 5000000 보장됨 ✅
  • lock.acquire() → 🔒 락 획득 후 실행
  • lock.release() → 🔓 락 해제
  • with lock: 구문을 사용하면 자동으로 락 관리

📌 정리

임계 구역(Critical Section) → 여러 스레드/프로세스가 공유 자원에 접근하는 코드 영역

경쟁 상태(Race Condition) → 여러 개의 작업이 동시에 접근하면 예측 불가한 결과 발생

해결 방법

  • 🔒 뮤텍스(Mutex) / 락(Lock) : → 하나의 스레드만 접근 가능하도록 제한
    • 뮤텍스,락은 자물쇠 개념이다. lock , unlock 다른 쓰레드가 접근 가능
    • Binary Lock (0또는 1) - 하나의 키(스레드)만 소유 가능
    • 뮤텍스는 엄격한 소유권 , 락은 범용적인 편 (여러개 설정 가능)
  • 🚦 세마포어(Semaphore) → 특정 개수의 스레드만 접근 가능하도록 제한
    • 뮤텍스와 달리 여러 개의 스레드가 동시 접근 가능
    • n개 리소스 관리 가능
    • 세마포어가 1 = 뮤텍스랑 같은 역할
    • 세마포어가 n = n개까지 최대 동시접근 가능

 

뮤텍스 (Mutex) 1개 (🔒 완전 잠금) O (하나의 스레드만 소유) 공유 변수 보호
락 (Lock) 1개 (🔒 완전 잠금) O (뮤텍스보다 범용적) 여러 개 락 가능
세마포어 (Semaphore) N개 (🚦 동시 접근 가능) X (소유권 없음) DB 연결 제한, API 요청 제한

밑에 사진은 , 구조가 위 사진과 같을 때 , 올바르지 않은 순서, 올바른 순서에 대한 예제다.

역방향으로 참조하게 되면 아직 쓰이지 않은 메모리를 읽으려 해서 문제가 생긴다.

즉 , A가 있더라도 B→공유메모리 부터 읽으면 A의 과거 데이터를 갖고 참조한다.

멀티 스레드/멀티 프로세스 환경에서 여러 개의 작업이 공유 자원에 동시에 접근 하면 문제가 될 수 있다. 그걸 보호 하기 위한 것이 임계구역이다.

레이스 컨디션 방지 하려면…

프로세스와 스레드가 동기화 되어야 한다.

동기화란?

  • 실행 순서 제어 : 프로세스 및 스레드를 올바른 순서로 실행하기
  • 상호 배제 : 동시에 접근해서는 안되는 자원에 하나의 프로세스 및 스레드만 접근하기

다시 말해, 동기화는 실행순서 제어를 위한 동기화 , 상호배제를 위한 동기화 가 있는 것이다.

📌 동기화 기법과 동작 원리 정리표

동기화 개념 설명 적용되는 기법 사용 예시

실행 순서 제어 ✅ 프로세스/스레드를 올바른 순서로 실행 🔹 **이벤트 (Event)**🔹 **조건 변수 (Condition Variable)**🔹 바리어 (Barrier) 이벤트: 특정 작업이 끝난 후 실행조건 변수: 생산자가 데이터를 만들기 전까지 소비자가 대기바리어: 모든 스레드가 도착할 때까지 대기
상호 배제 (Mutual Exclusion) 여러 스레드가 동시에 공유 자원에 접근하지 않도록 보호 🔹 **뮤텍스 (Mutex)**🔹 락 (Lock, RLock) 뮤텍스: 한 번에 하나의 스레드만 접근 가능락: 재진입 가능한 락(RLock) 사용 가능
동시 접근 제한 최대 N개 스레드만 자원에 접근 가능 🔹 세마포어 (Semaphore) N개의 데이터베이스 연결 제한API 요청 횟수 제한
데이터 공유 및 안전한 교환 멀티스레드 환경에서 안전한 데이터 전달 🔹 큐 (Queue) 생산자-소비자 모델 (Producer-Consumer)멀티스레드 작업 분배

동기화 기법(Synchronization Techniques)이란?

멀티스레드 또는 멀티프로세스 환경에서 공유 자원(데이터, 변수)에 대한 접근을 조절하는 방법

레이스 컨디션(Race Condition) 방지 & 데이터 일관성 유지

멀티스레딩/멀티프로세싱 환경에서 필수적인 개념

📌 동기화 개념, 설명, 사용 예시 정리표

적용 개념 설명 동기화 기법

상호 배제 (Mutual Exclusion) 여러 스레드가 동시에 공유 자원(변수, 파일 등)에 접근하는 것을 방지하여 데이터 일관성을 유지 뮤텍스(Mutex), 락(Lock)
동시 접근 제한 특정 자원(예: 데이터베이스, API 요청 등)에 대해 최대 N개 스레드만 접근 가능하도록 제한 세마포어 (Semaphore)
실행 순서 제어 특정 작업이 순서대로 실행되도록 보장하거나, 특정 조건이 충족될 때까지 실행을 대기하도록 동기화 이벤트 (Event), 조건 변수 (Condition Variable), 바리어 (Barrier)
데이터 공유 및 안전한 교환 여러 스레드가 데이터를 주고받을 때 경쟁 상태(Race Condition) 없이 안전하게 교환할 수 있도록 보장 큐 (Queue)

📌 동기화 기법 정리 (적용 개념 & 사용 예시 추가!)

동기화 기법 동작 원리 적용 개념 사용 예시 예제

뮤텍스 (Mutex) 🔒 하나의 스레드만 접근 가능 (락을 획득한 스레드만 임계 구역 실행 가능) 상호 배제 (Mutual Exclusion) 공유 자원 보호, 파일 쓰기 동기화 threading.Lock()
락 (Lock) 🔒 뮤텍스와 비슷하지만 재진입 가능(RLock) 상호 배제 (Mutual Exclusion) 한 스레드가 여러 번 락을 획득해야 하는 경우 threading.RLock()
세마포어 (Semaphore) 🚦 N개 스레드만 접근 가능 (제한된 리소스 관리 가능) 동시 접근 제한 DB 연결 개수 제한, API 요청 제한 threading.Semaphore(N)
이벤트 (Event) 이벤트 플래그가 set() 될 때까지 대기, 신호 기반 실행 실행 순서 제어 특정 작업이 끝난 후 실행, 신호 기반 동기화 threading.Event()
조건 변수 (Condition Variable) 특정 조건이 충족될 때까지 대기, 생산자-소비자 모델에 사용 실행 순서 제어 생산자가 데이터를 만들 때까지 소비자가 대기 threading.Condition()
바리어 (Barrier) N개의 스레드가 도착할 때까지 모든 스레드 대기 실행 순서 제어 병렬 연산에서 모든 스레드가 특정 지점에서 동기화 필요할 때 threading.Barrier(N)
큐 (Queue) 사용 📥 멀티스레드에서 데이터 안전하게 주고받음 (자동 락 처리) 데이터 공유 및 안전한 교환 생산자-소비자 모델, 멀티스레드 데이터 전송 queue.Queue()


📌 클래스 인스턴스(객체) 생성 과정 표

단계 코드 설명

1️⃣ 클래스 선언 class Lock: (파이썬 내부 구현) Lock 클래스가 파이썬 내부에서 정의되어 있음
2️⃣ 객체 생성 lock = threading.Lock() Lock 클래스의 생성자(Constructor, __init__()) 를 호출하여 lock 객체 생성
3️⃣ 객체 사용 lock.acquire() lock 객체가 가진 acquire() 메소드를 실행하여 락을 획득
4️⃣ 속성 확인 lock.locked() lock 객체의 현재 상태(락이 걸려 있는지)를 확인
5️⃣ 객체 해제 lock.release() lock 객체가 가진 release() 메소드를 실행하여 락을 해제

🔹 1️⃣ 뮤텍스(Mutex) - 단일 스레드만 접근 가능

동작 원리:

  • 한 번에 하나의 스레드만 임계 구역(Critical Section)에 접근 가능
  • lock.acquire() 로 락을 획득한 스레드는 다른 스레드가 접근할 수 없음
  • 작업이 끝나면 lock.release() 호출해서 다른 스레드가 접근할 수 있도록 해줌

💡 동작 흐름

1. 스레드 1: lock.acquire() → 🔒 락 획득
2. 스레드 2: lock.acquire() 대기 중... (🔴 대기 상태)
3. 스레드 1: 작업 수행 후 lock.release() → 🔓 락 해제
4. 스레드 2: lock.acquire() → 🔒 락 획득 후 실행

🔹 2️⃣ 락(Lock) - 뮤텍스와 비슷하지만 재진입 가능

동작 원리:

  • 기본적으로 뮤텍스와 동일하지만, threading.RLock() 을 사용하면 같은 스레드가 여러 번 락을 획득할 수 있음

💡 차이점

구분 Lock() RLock()

같은 스레드에서 여러 번 락 획득 가능? ❌ 불가능 (데드락 발생) ✅ 가능 (재진입 가능)

🔹 3️⃣ 세마포어(Semaphore) - N개 제한 가능

동작 원리:

  • 뮤텍스와 다르게 최대 N개 스레드가 동시에 접근 가능
  • semaphore.acquire() 호출하면 세마포어 값(N)을 1 감소
  • semaphore.release() 호출하면 세마포어 값(N)을 1 증가
  • 값이 0이면 다른 스레드는 대기 상태

이진 세마포(Binary Semaphore) vs 카운팅 세마포(Counting Semaphore) 차이

구분 설명 예제 (Semaphore(N))

이진 세마포 (Binary Semaphore) 🔒 N=1인 세마포어 → 뮤텍스(Mutex)처럼 동작 (한 번에 1개 스레드만 접근 가능) Semaphore(1)
카운팅 세마포 (Counting Semaphore) 🚦 N>1인 세마포어 → 동시에 최대 N개 스레드 접근 가능 Semaphore(N) (ex: Semaphore(3))
  • 이진 세마포 → Mutex처럼 1개 스레드만 접근 가능 (N=1)
  • 카운팅 세마포최대 N개 스레드 접근 가능 (N>1)

세마포라는 용어는 일반적으로 카운팅 세마포

✅ 실행 흐름 (N=3 기준)

시점 실행 중인 스레드 N 값 (세마포어) 설명

시작 없음 3 모든 자원이 사용 가능
s1 실행 (acquire()) s1 2 s1이 세마포어를 1 감소
s2 실행 (acquire()) s1, s2 1 s2가 세마포어를 1 감소
s3 실행 (acquire()) s1, s2, s3 0 s3가 세마포어를 1 감소 (자원 없음)
s4 실행 시도 s1, s2, s3 0 s4는 semaphore.acquire()에서 대기
s1 종료 (release()) s2, s3 1 s1이 작업을 끝내고 세마포어 반환
s4 실행 (acquire()) s2, s3, s4 0 s4가 실행됨
s2 종료 (release()) s3, s4 1 s2가 세마포어 반환
s5 실행 (acquire()) s3, s4, s5 0 s5가 실행됨

🔹 4️⃣ 이벤트(Event) - 특정 신호를 받을 때 실행

동작 원리:

  • event.wait() 를 호출하면 이벤트가 set() 될 때까지 대기
  • event.set() 호출 시 모든 대기 중인 스레드가 실행됨

💡 동작 흐름

1. 스레드 1: event.wait() → ⏳ 이벤트 대기 중...
2. 스레드 2: event.wait() → ⏳ 이벤트 대기 중...
3. 메인 스레드: 5초 후 event.set() 실행 → 🟢 대기 중인 스레드 실행됨!

🔹 5️⃣ 조건 변수(Condition Variable) - 특정 조건 만족 시 실행

동작 원리:

  • condition.wait() → 특정 조건이 충족될 때까지 스레드 대기
  • condition.notify() → 대기 중인 스레드를 깨워 실행

💡 동작 흐름 (생산자-소비자 모델)

1. 소비자: condition.wait() → 📦 데이터가 생성될 때까지 대기
2. 생산자: condition.notify() → ✅ 소비자에게 신호 전달 (데이터 사용 가능)
3. 소비자: 🚀 실행 후 데이터 소비!

모니터(Monitor) 기법이란?

뮤텍스 + 조건 변수의 조합 → 한 번에 하나의 스레드만 접근 가능하도록 관리하는 동기화 기법

객체 지향 프로그래밍에서 멀티스레드를 안전하게 관리하는 방법

자동으로 락을 걸고, 특정 조건을 만족할 때까지 기다리는 구조

자바, 파이썬에서 synchronized, threading.Condition() 같은 기능이 모니터 개념을 활용함

프로세스 및 스레드는 공유자원에 접근 시에 반드시 정해진 공유자원연산(인터페이스)를 통해 모니터 내로 진입해야 하고, 모니터 안에 진입하여 실행되는 프로세스 및 쓰레드는 항상 1개 여야 한다.

이미 모니터 내로 진입하여 실행 중인 프로세스 및 스레드가 있다면 큐에서 대기 해야 한다.


🔹 6️⃣ 바리어(Barrier) - 모든 스레드가 도착할 때까지 대기

동작 원리:

  • barrier.wait() 을 호출하면 N개의 스레드가 도착할 때까지 모든 스레드가 대기
  • 모든 스레드가 도착하면 한꺼번에 실행됨

💡 동작 흐름 (Barrier(3))

1. 스레드 1: barrier.wait() → ⏳ 대기 중...
2. 스레드 2: barrier.wait() → ⏳ 대기 중...
3. 스레드 3: barrier.wait() → 🚀 모든 스레드 실행 시작!

🔹 7️⃣ 큐(Queue) - 멀티스레드에서 데이터 공유

큐(Queue)

  • FIFO(First In, First Out, 선입선출) 방식의 자료구조
  • 멀티스레드 환경에서 안전하게 데이터를 주고받을 때 사용
  • 락(Lock) 없이 자동 동기화 지원 (queue.Queue)

 

동작 원리:

  • 멀티스레드 환경에서 안전하게 데이터를 주고받을 수 있도록 queue.Queue() 사용
  • put() → 데이터를 큐에 삽입
  • get() → 데이터를 큐에서 꺼냄 (자동으로 락 관리됨)

📌 큐 기법

큐 종류 설명 예제

일반 큐 (queue.Queue) FIFO 방식으로 동작 (스레드 안전) queue.Queue()
우선순위 큐 (queue.PriorityQueue) 우선순위 높은 항목이 먼저 처리됨 queue.PriorityQueue()
LIFO 큐 (queue.LifoQueue) 스택(Stack)처럼 동작 (후입선출) queue.LifoQueue()

( 가정 ) 프로세스 A 먼저 실행 , B가 실행되어야 한다

만약 B가 먼저 모니터 내로 진입 했을 경우 이 가정이 어긋난다.

따라서 특정 조건변수(cv)에 대해 cv.wait을 호출해 B를 대기 상태로 만든다.

그 후 A가 끝나면 다시 재진입 한다.


쓰레드 안전 (멀티스레드에서 데이터 보호)

멀티 스레드 환경에서 어떤 변수나 함수, 객체에 동시 접근이 이루어져도 실행에 문제가 없는 상태

  • 여러 개의 스레드가 동시에 접근해도 문제가 발생하지 않는 코드 또는 자료구조
  • 경쟁 상태(Race Condition)를 방지하는 기법
  • 락(Lock) 없이도 안전한 자료구조 활용 (queue.Queue() 등)

스레드 안전을 보장하는 기법은 위에 7가지 중에 사용하면 된다.


교착 상태(Dead lock) - 쓰레드가 멈췄다ㅠ

  • 두 개 이상의 스레드(또는 프로세스)가 서로의 리소스를 기다리면서 영원히 멈추는 상태
  • "A가 B를 기다리고, B가 A를 기다리는 경우" 발생
  • 락을 잘못 사용하면 교착 상태가 생김
  • 4가지 모두 만족되면 교착 상태가 일어날 가능성이 생긴다.

A와 B중에 누가 먼저 해야할지 모르는 상황

교착상태는 왜 생기는데? (발생 조건)

💡 4가지 조건이 모두 만족되면 교착 상태 가능성이 발생

조건 설명 보충 설명

1️⃣ 상호 배제 (Mutual Exclusion) (주요 원인) 한 번에 하나의 프로세스만 자원 사용 가능 한 프로세스가 자원을 점유하면, 다른 프로세스는 접근 불가
2️⃣ 점유와 대기 (Hold and Wait) 한 프로세스가 자원을 점유한 채, 추가 자원을 기다리는 상황 점유 중인 자원을 해제하지 않고, 다른 자원을 기다림
**3️⃣ 비선점 (No Preemption)    
(주요 원인)** 프로세스가 자원을 강제로 빼앗길 수 없음 프로세스가 사용 중인 자원을 강제로 회수할 방법이 없음
4️⃣ 원형 대기 (Circular Wait) 자원이 원을 이루면서 서로 점유 & 대기 P1 → P2 → P3 → P1 형태로 무한 루프

교착상태 해결 방법

  • 예방 : 애초에 교착 상태의 발생조건에 부합하지 않도록 자원을 분배
  • 회피 : 교착상태가 발생하지 않을 정도록 조금씩 자원 할당 & 위험하면 자원 미할당
  • 검출 후 회복 : 자원을 제약없이 할당 → 교챡 상태를 검출한 후 회복(자원 선점이나 강제종료)

'Study > CS' 카테고리의 다른 글

03 - 5 가상 메모리  (0) 2025.02.19
03 - 4 CPU 스케줄링  (0) 2025.02.19
03-2 프로세스와 스레드  (1) 2025.02.12
03-1 운영체제 큰그림  (0) 2025.02.12
02-5 보조기억장치와 입출력 장치  (0) 2025.02.05