경쟁상태 해결 조건
여러 프로세스나 스레드가 공유 자원에 접근할 때 데이터의 일관성과 무결성을 보장하기 위해 아래와 같은 조건을 만족해야한다.
상호배제 (Mutual Exclusion)
임계영역에 한번에 하나의 프로세스나 스레드만 접근할 수 있도록 해야함.
즉, 어떤 프로세스가 임계영역에 있을 때 다른 프로세스는 그 임계영역에 들어갈 수 없음.
진행의 융통성(Progress)
임계영역에 들어가려는 프로세스가 없으면, 임계영역에 들어갈 수 있는 프로세스가 이를 결정할 수 있어야 함.
즉, 임계영역에 들어갈 수 있는 프로세스를 선택하는 과정이 지연되지 않고 진행되어야 함.
결정은 임계영역에 있지 않은 프로세스만 참여할 수 있음.
한정 대기 (Bounded Wating)
특정 프로세스가 임계영역에 들어가는 것이 무한정 지연되면 안됨.
어떤 프로세스가 임계영역에 들어가려고 요청한 이후, 그 프로세스가 임계영역에 들어갈 수 있을 때까지 다른 프로세스들이 임계영역에 들어가는 횟수는 제한시켜 특정 프로세스가 영원히 임계영역에 들어가지 못하는것을 방지.
이 조건을 잘 지키며, 공정성을 잘 지켜지는 경쟁상태를 해결하는 대표적 방법으로는 뮤텍스, 세마포어, 모니터가 있다.
공정성? 여러 스레드나 프로세스가 경쟁상태에 있을 때 특정 스레드나 프로세스가 불공평하게 오래 기다리지 않도록 하는 성질.
뮤텍스 (Mutex)
객체 lock을 기반으로 경쟁상태를 해결함
뮤텍스를 lock()을 통해 잠금 설정하고 사용한 후에 unlock()을 통해 잠금해제가 됨.
한 프로세스나 스레드가 잠금을 설정하면 다른 프로세스나 스레드는 잠긴 코드 영역에 접근할 수 없다.
뮤텍스가 잠금된 것을 뮤텍스를 획득한다고 함.
Java에서 뮤텍스 예제
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexExample {
private int counter = 0;
private final Lock lock = new ReentrantLock(); // ReentrantLock 객체를 생성하여 뮤텍스 초기화
public void increment() {
lock.lock(); // 뮤텍스 획득(잠금)
try {
counter++; // 임계 영역
} finally {
lock.unlock(); // 뮤텍스 해제
}
}
public int getValue() {
return counter;
}
public static void main(String[] args) {
MutexExample example = new MutexExample();
// task를 실행하면 increament() 메서드를 100000번 실행
Runnable task = () -> {
for (int i = 0; i < 100000; i++) {
example.increment();
}
};
// thread1, thread2 두개의 스레드가 task 실행
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join(); // thread1의 작업이 완료될 때까지 기다림
thread2.join(); // thread2의 작업이 완료될 때까지 기다림
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + example.getValue());
}
}
ReentrantLock은 Java에서 제공하는 lock객체이다. 일종의 뮤텍스로 ReentrantLock은 스레드가 여러번 잠금을 획득할 수 있는 기능(재진입 기능)을 제공한다. synchronized에서는 제공하지 않는 공정성 옵션을 제공한다. 공정모드에서는 대기중인 스레드가 순서대로 락을 획득한다.
세마포어 (Semaphore)
여러 프로세스나 스레드가 동시에 임계영역에 접근할 수 있다. <- 상호배제를 명시적으로 구현해야함.
대신 임계영역에 접근할 수 있는 최대 수를 제어함으로써, 경쟁상태를 해결한다.
(예시, DB연결 커넥션)
일반화된 뮤텍스. 간단한 정수 S와 wait(), signal() 메서드로 공유 자원에 대한 접근을 처리한다.
- S는 현재 쓸 수 있는 공유자원의 수.
- wait()은 S를 1씩 감소시킴. 자원 요청 시 호출.
- 자원 요청을 위해 wait()을 호출했을 때 S가 음수가 되면? 사용 가능한 자원이 없다는 뜻. 공유자원을 쓸 수 없음.
-> 접근 요청한 프로세스는 준비 큐에 집어 넣고 자원이 해제될 때까지 기다림. - S가 0 이상이면 프로세스는 자원을 사용할 수 있다는 뜻. 아무런 작업을 하지 않고 반환.
- 자원 요청을 위해 wait()을 호출했을 때 S가 음수가 되면? 사용 가능한 자원이 없다는 뜻. 공유자원을 쓸 수 없음.
- signal()은 S를 1씩 증가시킴. 자원을 해제 시 호출.
- 자원 해제를 위해 signal()을 호출했을 때 S가 0이하라면? 준비 큐에 대기 중인 프로세스가 있다는 얘기.
-> 대기열에 있던 프로세스를 끄집어 내서 공유자원에 대한 작업을 진행 함. - S가 0보다 크면 대기중인 프로세스가 없다는 뜻. 아무런 작업을 하지 않고 반환.
- 자원 해제를 위해 signal()을 호출했을 때 S가 0이하라면? 준비 큐에 대기 중인 프로세스가 있다는 얘기.
Java에서 세마포어 예제
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final int MAX_PERMITS = 1; // 최대 1개의 자원 허용
private final Semaphore semaphore = new Semaphore(MAX_PERMITS); // 세마포어 생성
public void accessResource() {
try {
semaphore.acquire(); // wait() 연산: 세마포어 값을 1 감소시키고, 세마포어 값이 음수면 대기열에 추가
System.out.println(Thread.currentThread().getName() + " is accessing the resource");
// 자원에 접근
Thread.sleep(1000); // 자원을 사용하는 시간 (예: 파일 읽기/쓰기)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println(Thread.currentThread().getName() + " is releasing the resource");
semaphore.release(); // signal() 연산: 세마포어 값을 1 증가시키고, 대기열에서 프로세스 깨움
}
}
public static void main(String[] args) {
SemaphoreExample example = new SemaphoreExample();
Runnable task = example::accessResource;
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
자바에서는 acquire()가 wait() 역할을 하고, release()가 signal()역할을 한다.
세마포어 종류
- 이진(바이너리) 세마포어
- 세마포어가 0,1 두가지 값만 가질 수 있음.
- 뮤텍스와 유사하게 동작하지만 뮤텍스는 '잠금 메커니즘', 세마포어는 '신호 메커니즘'
- 카운팅 세마포어
- 여러개의 값을 가질 수 있는 세마포어.
모니터 (Monitor)
공유자원을 숨기고 해당 접근에 대해 인터페이스만 제공하는 객체이다.
언어수준에서 제공되는 동기화 수단이다. 고수준의 동기화 수단이다.
ㄴ 세마포어나 뮤텍스보다 사용하기 쉽고 오류가 적다.
뮤텍스와 조건변수를 결합한 형태이다.
한번에 하나의 프로세스만 공유자원에 접근할 수 있다. <- 상호배제가 자동.
모니터는 '공유변수', '동기화된 메서드', '조건변수'로 구성된다.
- 공유변수
모니터 내에서 관리되고 보호되는 변수들. 외부에서 직접 접근 불가. - 동기화된 메서드(Procedure/Function)
공유자원에 접근/조작하는데 사용되는 메서드. 한번에 하나의 프로세스/스레드만 실행 가능한 메서드. - 조건변수
프로세스가 특정 조건을 기다리거나 신호를 보낼 때 사용. wait(), signal(), notify(), notifyAll() 등의 연산을 지원.
Java에서 모니터 예제
class MonitorExample {
private int count = 0;
/*
아래 함수들을 synchronized키워드에 의해
한 스레드가 count를 변경중이면 다른 스레드가 count를 변경하지 못하게 하고
동기화되어 있으므로 변경되는 동안 값을 안전하게 읽을 수 있게 함.
*/
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
// wait와 notify를 사용한 동기화 블록
public void exampleMethod() {
// 아래 블록은 한번에 하나의 스레드만 진입 가능. 특정 조건이 만족할 때 까지 대기.
synchronized (this) {
while (count == 0) { // count가 0일때는 대기
try {
wait(); // 현재 스레드를 일시정지 상태로 만듦
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 발생 시 스레드를 활성화
}
}
// count를 사용하여 어떤 작업 수행
count--;
notify(); // 대기 중인 다른 스레드에 알림
}
}
}
자바에서는 synchronized 키워드와 wait(), notify(), notifyAll() 메서드를 사용하여 모니터를 구현할 수 있다.
synchronized 키워드를 사용하여 increment(), decrement(), getCount() 메서드를 동기화한다. 한번에 하나의 스레드만 이 메서드들 을 실행할 수 있다.
'Computer Science > Operating System' 카테고리의 다른 글
캐시 종류와 캐시 매핑( 직접매핑, 연관매핑, 집합-연관 매핑) (0) | 2024.11.03 |
---|---|
교착 상태(deadlock)와 은행원 알고리즘 (0) | 2024.11.02 |
공유자원과 경쟁 상태, 임계영역 (1) | 2024.10.31 |
멀티태스킹(Multitasking)과 CPU 스케줄링(선점형, 비선점형, 라운드로빈, 우선순위 기반 스케줄링, FIFO, SFJ ...) (0) | 2024.10.30 |
프로세스의 상태 (0) | 2024.10.29 |