공유 자원
프로세스나 스레드가 공유하는 자원
공유자원은 메모리나 파일이 될 수도 있고 , 전역변수나 입출력장치가 될 수 있다.
임계 구역(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 |