📀 운영체제
14. 모니터와 자바 동기화: Chapter 6. Synchronization Tools (Part 4)
락꿈사
2022. 6. 13. 17:57
6.7 모니터
- mutex락과 세마포는 타이밍 오류를 야기할 수 있음
- 예시
- 모든 프로세스는 mutex라는 이진 세마포 변수를 공유함
- mutex 변수의 초기값은 1
- 각 프로세스는 임계구역에 진입하기 전에 wait(mutex)를 실행해야 함
- 각 프로세스는 임계구역을 나올 때 signal(mutex)를 실행해야 함
- 가정 1) 예시 프로세스의 순서가 지켜지지 않을 경우 (wait()과 signal() 연산의 순서가 뒤바뀌는 경우)
- 두 프로세스가 동시에 임계구역에 있을 수 있음 (상호 배제 요구조건 위반)
- 예시 코드
signal(mutex);
...
cirical section
...
wait(mutex);
- 가정 2) signal(mutex)를 써야 할 곳에 잘못해서 wait(mutex)를 쓴 경우
- 세마포를 사용할 수 없으므로 두 번째 wait() 호출에서 데드락에 걸림
- 예시 코드
wait(mutex);
...
ciritical section
...
wait(mutex);
- 가정 3) wait(mutex)나 signal(mutex) 또는 둘 다를 빠뜨렸을 경우
- 상호 배제 요구 조건을 위반하거나 데드락에 걸림
- 이러한 오류를 처리하기 위한 한가지 전략은 간단한 동기화 도구를 통합하여 고급 언어 구조물을 제공하는 것
- 모니터는 이러한 고급 언어 구조물 중 하나임
모니터 사용법
- 추상화된 데이터형ADT(Abstract data type)은 데이터와 이 데이터를 조작하는 함수들의 집합을 하나의 단위로 묶어 보호함
- 모니터형
- 상호 배제가 보장됨
- 프로그래머가 정의한 일련의 연산자 집합을 포함하는 ADT
- 변수 선언 포함
- 변수들의 값은 그 형에 해당하는 인스턴스의 상태를 정의함
- 변수들을 조작할 수 있는 프로시저 또는 함수들의 본채도 포함
- 모니터ADT는 모니터 안에 항상 하나의 프로세스만이 활성화되도록 보장함
- condition 변수를 갖는 모니터
- 구현
- condition 변수 x, y가 존재
- 변수 x,y에 대한 준비 큐가 분리되어 operation들이 각각의 변수에 대해서 처리가 되고 동기화 됨
- wait() 연산
- condition형 변수에 호출될 수 있는 유일한 연산
- x.wait(), y.wait()으로 사용
- x.signal(), y.signal()이 호출될 때 까지 일시 중지 되어야 함을 의미
- signal() 연산
- condition형 변수에 호출될 수 있는 유일한 연산
- x.signal(), y.signal()으로 사용
- 각각의 변수의 준비큐에 있는 하나의 일시 중시 프로세스를 재개함
- 일시 중지 프로세스가 없을 경우 signal() 연산은 효과가 없음
- Java의 모니터
- 자바에서는 모니터 락을 사용함
- 스레드 동기화을 위한 concurrency 매커니즘
- monitor lock, intrinsic lock이라고 부름
- synchronized 키워드를 사용
- 임계영역에 해당하는 코드 블록을 선언할 때 사용하는 자바 키워드
- 해당 코드 블록(임계영역)에는 모니터락을 획득해야 진입 가능
- 메소드에 선언하면 메소드 코드 블록 전체가 임계영역으로 지정됨
- 이 때 모니터락을 가진 객체 인스턴스는 this 객체 인스턴스임
- 아래 코드 예시 1), 예시 2) 참고
- wait() / notify() 메소드 사용
- java.lang.Object 클래스에 선언됨 (모든 자바 객체가 가진 메소드)
- 어떤 스레드가 어떤 객체의 wait() 메소드를 호출
- 해당 객체의 모니터 락을 획득하기 위해 대기 상태로 진입함
- 어떤 스레드가 어떤 객체의 notify() 메소드를 호출
- 해당 객체의 모니터에 대기중인 스레드 하나를 깨움
- 어떤 스레드가 어떤 객체의 notifyAll() 메소드를 호출
- 해당 객체 모니터에 대기중인 스레드 전부를 깨우
- 자바에서는 모니터 락을 사용함
- 구현
예시 1)
synchronized (object) {
// critical section
}
예시 2)
public synchronized void add() {
// critical section
}
- 예제 1
- 모니터 락을 사용하지 않았으므로 동기화 문제 발생
// 방법 1) 동기화 문제 발생
// race condition 문제 존재
package com.os_study;
public class SynchExample1 {
// 모니터
static class Counter{
public static int count = 0;
public static void increment(){
count++;
}
}
static class MyRunnable implements Runnable{
@Override
public void run(){
for (int i=0; i<10000; i++){
Counter.increment();
}
}
}
public static void main(String[] args) throws Exception{
Thread threads[] = new Thread[5];
for (int i=0; i<threads.length; i++){
threads[i] = new Thread(new MyRunnable());
threads[i].start();
}
for (int i=0; i< threads.length; i++)
threads[i].join();
System.out.println("counter =" + Counter.count);
}
}
- 예제 2
- 모니터 락을 사용하여 동기화 문제 해결
- 메소드를 임계영역으로 만듦
// 방법 2) 동기화 문제 해결
package com.os_study;
public class SynchExample2 {
// 모니터
static class Counter{
public static int count = 0;
// acquire() 파트
// synchronized를 사용하여 메서드를 임계영역으로 만들어 줌
// 메소드가 길어질 경우 멀티쓰레드의 효율성이 떨어짐
synchronized public static void increment(){
count++;
}
// realease() 파트
}
static class MyRunnable implements Runnable{
@Override
public void run(){
for (int i=0; i<10000; i++){
Counter.increment();
}
}
}
public static void main(String[] args) throws Exception{
Thread threads[] = new Thread[5];
for (int i=0; i<threads.length; i++){
threads[i] = new Thread(new MyRunnable());
threads[i].start();
}
for (int i=0; i< threads.length; i++)
threads[i].join();
System.out.println("counter =" + Counter.count);
}
}
- 예제 3
- 모니터 락을 사용하여 동기화 문제 해결
- 메소드 안의 공유 변수에 접근하는 부분만 임계영역으로 지정
// 방법 3) 동기화 문제 해결
package com.os_study;
public class SynchExample3 {
// 모니터
static class Counter{
// dummy 오브젝트를 생성하여 사용
private static Object object = new Object();
public static int count = 0;
public static void increment(){
synchronized (object){
Counter.count++;
}
}
}
static class MyRunnable implements Runnable{
Counter counter;
public MyRunnable(Counter counter){
this.counter = counter;
}
@Override
public void run(){
for (int i=0; i<10000; i++){
counter.increment();
}
}
}
public static void main(String[] args) throws Exception{
Thread threads[] = new Thread[5];
for (int i=0; i<threads.length; i++){
threads[i] = new Thread(new MyRunnable(new Counter()));
threads[i].start();
}
for (int i=0; i< threads.length; i++)
threads[i].join();
System.out.println("counter =" + Counter.count);
}
}
- 예제 4
- 각 스레드가 서로 다른 Counter 객체(모니터 객체)를 참조함으로 인한 스레드 각각의 동기화가 이루어짐
- static 변수 count에 대한 동기화가 이루어지지 않았으므로 동기화 문제 발생
// 방법 4) 동기화 문제 존재
// 쓰레드끼리의 동기화가 아니라 쓰레드 각각의 동기화가 이루어짐으로 인한 문제
package com.os_study;
public class SynchExample4 {
// 모니터
static class Counter{
// 인스턴스의 객체 참조 함수를 사용하여 임계영역에 접근해주는 방법
// this. 키워드를 사용하여 자기 객체 인스턴스의 모니터 락을 획득
public static int count = 0;
public void increment(){
synchronized (this){
Counter.count++;
}
}
}
static class MyRunnable implements Runnable{
Counter counter;
public MyRunnable(Counter counter){
this.counter = counter;
}
@Override
public void run(){
for (int i=0; i<10000; i++){
// 인스턴스의 메소드로 접근
counter.increment();
}
}
}
public static void main(String[] args) throws Exception{
Thread threads[] = new Thread[5];
for (int i=0; i<threads.length; i++){
// Counter 객체 인스턴스, 즉 모니터 인스턴스가 5개가 생김
// 모니터 안의 모든 메소드는 모니터 안 변수의 동기화를 의미함
// 객체가 달라지면 모니터가 다 따로 생기는 것이므로 모니터끼리의 동기화는 이루어지지 않음
// 동기화 문제 발생
threads[i] = new Thread(new MyRunnable(new Counter()));
threads[i].start();
}
for (int i=0; i< threads.length; i++)
threads[i].join();
System.out.println("counter =" + Counter.count);
}
}
- 예제 5
- 각 스레드가 하나의 Counter 객체(모니터 객체)를 참조함으로 인한 스레드 끼리의 동기화가 이루어짐
- static 변수 count에 대한 동기화가 이루어져 동기화 문제 해결
// 방법 5) 동기화 문제 해결
package com.os_study;
public class SynchExample5 {
// 모니터
static class Counter{
// 인스턴스의 객체 참조 함수를 사용하여 임계영역에 접근해주는 방법
// this. 키워드를 사용하여 자기 객체 인스턴스의 모니터 락을 획득
public static int count = 0;
public void increment(){
synchronized (this){
Counter.count++;
}
}
}
static class MyRunnable implements Runnable{
Counter counter = new Counter();
public MyRunnable(Counter counter){
this.counter = counter;
}
@Override
public void run(){
for (int i=0; i<10000; i++){
counter.increment();
}
}
}
public static void main(String[] args) throws Exception{
Thread threads[] = new Thread[5];
Counter counter = new Counter();
for (int i=0; i<threads.length; i++){
// Counter 객체 인스턴스, 즉 모니터 인스턴스가 5개가 생김
// 모니터 인스턴스가 위에서 생성한 하나의 Counter 객체를 가지고 있음
// 그 Counter 객체는 static count 변수에 접근함
// 동기화 문제가 발생하지 않음
threads[i] = new Thread(new MyRunnable(counter));
threads[i].start();
}
for (int i=0; i< threads.length; i++)
threads[i].join();
System.out.println("counter =" + Counter.count);
}
}
6.6 라이브니스Livness
- 위의 동기화 도구들을 사용함으로써 발생할 수 있는 결과는 교착상태deadlock이 될 수 있음
- 데드락은 임계구역 문제 해결 조건 중 진행progress과 한정된 대기bounded waiting을 위반함
- 라이브니스
- 프로세스가 실행 수명주기 동안 진행되는 것을 보장하기 위해 시스템이 충족해야 하는 일련의 속성을 말함
- deadlock은 라이브니스 실패의 예
교착 상태deadlock
- 교착 상태
- 두 개 이상의 프로세스들이 오로지 대기 중인 프로세스들 중 하나에 의해서만 야기될 수 있는 이벤트를 무한정 기다리는 상황
- 예시
- 위 그림에서 P0이 wait(S)를 실행하고 P1이 wait(Q)를 실행한다고 가정
- P0이 wait(S)를 실행할 때 P0은 P1이 signal(S)를 실행할 때 까지 기다려야 함
- P1이 wait(Q)를 실행할 때 P1은 P0이 signal(Q)를 실행할 때 까지 기다려야 함
- 이들 signal() 연산들은 실행될 수 없기 때문에 P0과 P1은 교착 상태가 됨
- 위 그림에서 P0이 wait(S)를 실행하고 P1이 wait(Q)를 실행한다고 가정
우선순위 역전
- 우선순위 역전Priority inversion문제
- 높은 우선순위 프로세스가 현재 낮은 우선순위 프로세스 또는 연속된 낮은 우선순위 프로세스에 의해 접근되고 있는 커널 데이터를 읽거나 변경하려고 할 때 스케쥴링의 어려움이 발생
- 통상 커널 데이터는 락에 의해 보호되기 때문에 낮은 우선순위 프로세스가 자원의 사용을 마칠 때까지 높은 우선순위 프로세스가 기다려야 함
- 예시
- 우선순위가 L < M < H 순서인 L, M, H 세 개의 프로세스가 있을 때
- 프로세스 H가 세마포어 S가 필요
- 자원 R은 현재 L에 의해 접근되고 있다고 가정
- 프로세스 H는 L이 자원 사용을 마칠 때 까지 기다림
- 이 순간 프로세스 M이 실행 가능 상태가 되어 자원 R을 선점함
- 프로세스 M은 프로세스 H가 L이 자원을 양도할 때 까지 기다려야 하는 시간에 영향을 줌
- 우선순위 상속 프로토콜priority-inheritance protocol
- 더 높은 우선순위 프로세스가 필요로 하는 자원에 접근하는 모든 프로세스는 해당 자원의 사용이 끝날 때 까지 더 높은 우선순위를 상속받음
- 따라서 위 예시에서 프로세스 M이 L의 실행을 선점하는 것을 방지함
- 프로세스 L이 자원 R의 사용을 마치면 상속받은 우선순위를 방출하고 원래 우선순위로 되돌아감
- 자원 R은 프로세스 H가 다음에 실행됨