본문 바로가기
공부(~2016)/멀티쓰레드

[멀티쓰레드패턴] #4. Balking 패턴

by soy; 2015. 1. 10.

이 내용은 "Java 언어로 배우는 디자인 패턴 입문 - 멀티쓰레드편" 책의 Chapter 04 부분을 정리한 것이다.


Balking 패턴

- balk : 중단하고 돌아가다

- 어떤 처리를 할 때, 실행 직전에 가드조건을 확인하여 조건을 만족하지 않을 때에는 처리를 하지 않고 그냥 돌아가는 패턴


- 객체는 상태를 가지고 있는데, 상태가 적절할 때에만 처리를 실행하고 상태가 적절하ㄴ지 않으면 처리를 실행하지 않는다.

1. 객체가 적절한 상태임을 가드조건으로 표현한다. 

2. 처리하기 전에 가드조건을 만족하는지 테스트한다. (if문) 가드조건 테스트는 synchronized 메소드/블록 안에서 수행되어야 한다.

3. 가드조건을 만족하는 경우에 한해 실행을 계속한다.

4. 가드조건을 만족하지 않으면 처리를 실행하지 않고 곧바로 돌아간다. (return 혹은 throw로 예외통보)


import java.io.IOException;
import java.io.FileWriter;
import java.io.Writer;
 
public class Data {
    private final String filename;
    private String content;
    private boolean changed;        // flag 필드
 
    public Data(String filename, String content) {
        this.filename = filename;
        this.content = content;
        this.changed = true;
    }
 
    // content 필드의 내용을 변경하는 메소드. 변경 후에는 플래그 필드를 true로 셋팅함.
    public synchronized void change(String newContent) {
        content = newContent;
        changed = true;
    }
 
    // content 필드의 내용이 변경되었다면 파일에 저장하는 메소드.
    public synchronized void save() throws IOException {
        if (!changed) {       // 가드조건 확인. 플래그 필드가 true일 때에만 doSave() 메소드를 호출.
            return;           // 플래그 필드가 false라면, Balking 함
        }
        doSave();
        changed = false;
    }
 
    // 
    private void doSave() throws IOException {
        System.out.println(Thread.currentThread().getName() + " calls doSave, content = " + content);
        Writer writer = new FileWriter(filename);
        writer.write(content);
        writer.close();
    }
}

- content 필드와 changed 필드를 동시에 접근하지 못하도록 change 메소드와 save 메소드는 synchronized로 선언됨.

- change 메소드는 content 필드의 내용을 변경한 후, 플래그 필드를 true로 셋팅함.


- save 메소드는 플래그 필드가 true라면 doSave() 메소드를 호출하여 파일 write를 함.

- 플래그 필드가 false라면 파일 write를 할 필요가 없으므로 아무것도 하지 않고 돌아감 (Balking 함)

- 이 예제에서는 Balking 된 경우, save 메소드를 호출하는 측에 balk 사실을 따로 알리지 않는다. balk 발생 여부를 전달할 필요가 있는 경우는 null값을 리턴하거나 boolean 값을 리턴함으로써 나타낸다. (예를 들면, 처리가 수행된 경우는 return true; 처리가 수행되지 않고 balk 된 경우에는 return fals;) 혹은 예외를 throw하여 전달하는 경우도 있다.



Balking 패턴을 사용하는 경우

1. (조건을 만족하지 않을 때에는) 굳이 처리할 필요가 없는 경우


2. 조건이 충족되기를 기다리고 싶지 않은 경우

- Balking 패턴의 특징은 기다리지 않는다는 데에 있다. 조건을 만족하지 않으면 바로 돌아가서 다음 작업을 진행하겠다고 하는 경우에 사용하면 응답성을 높일 수 있다.


3. 조건을 만족하는 것이 처음 1회뿐인 경우

 

public class Something {
    private boolean initialized = false;  // 가드조건 flag
 
    public synchronized void init() {
        if (initialized) {    // 가드조건 확인
            return;           // 조건을 만족하지 않는다면 Balking
        }
        doInit();             // 가드조건을 만족하는 경우 처리 수행
        initialized = true;
    }
    private void doInit() {
        // 실제 초기화 처리
    }
}

이 클래스에서는 initialized 필드가 초기화 수행 여부를 나타낸다. init() 메소드를 호출하면 먼저 initialized 필드의 값을 조사하는데..

- 값이 true라면 이미 초기화가 끝난 것이므로 중단하고 돌아간다. (Balking)

- 값이 false라면 초기화 처리를 수행하고, 그 후 initialized 필드를 true로 셋팅하여 초기화가 완료되었음을 기록한다.


여기서는 가드조건을 while문이 아닌 if문으로 확인한다. 가드조건을 나타내는 필드(initialized)는 true가 된 다음에는 두 번 다시 false로 변경되지 않기 때문이다. 즉 이 클래스에서는 한 번 충족되지 못한 가드조건이 다시 충족되는 일은 절대 없다.


참고사항: initialized 필드처럼 '단 한 번만 상태가 변하는 변수'를 일반적으로 래치(latch)라고 한다. 빗장을 한 번 걸어두면 다시는 열리지 않기 때문이다.



관련 패턴

Balking 패턴

- Guarded Suspension 패턴에서 쓰레드는 가드조건이 충족될때까지 기다린다.

- Balking 패턴에서 쓰레드는 가드조건이 충족되는 것을 기다리지 않고 돌아간다.

- Guarded Suspension과 Balking의 중간 단계로서, 타임아웃이 있다.


Observer 패턴

Observer 패턴을 멀티쓰레드 환경에서 사용할 때 Balking 패턴을 이용하는 경우가 있다. Subject 역할이 Observer 역할에게 상태의 변화를 통보할 때, Observer 역할이 그 통보를 처리할 수 없는 상태라면 통보 처리를 balking 함.



참고) 타임아웃

- 가드조건을 만족하지 못한다면 일정 시간동안 기다린다. 시간이 모두 경과한 후에도 조건을 만족하지 못한 경우는 balking 하는 방법.

- wait 메소드를 호출할 때 인자로 타임아웃 시간을 지정하면 해당 시간이 경과했을 때 스스로 깨어나게 되는데, 이를 이용하여 구현한다.


wait 메소드는 언제 종료되는가

- wait() 메소드 호출시에 인자로 지정된 타임아웃 시간을 경과한 경우.

- 다른 쓰레드의 notify() / notifyAll() 메소드 호출에 의해 깨어난다.

- 다른 쓰레드의 interrupt() 메소드 호출에 의해 깨어난다.


참고사항

- notify와 notifyAll 메소드는 인스턴스를 대상으로 발생하나, interrupt는 쓰레드를 대상으로 발생한다.

- wait 메소드에서는 notify/notifyAll 되어서 깨어난 것인지, 타임아웃 되어 깨어난 것인지를 구별할 방법이 없다.


 

import java.util.concurrent.TimeoutException;
 
public class Host {
    private final long timeout = 10000;   // 타임아웃 값을 10초로 지정 
    private boolean ready = false;        // 가드조건 flag
 
    // 가드조건 flag의 상태를 변경하고 다른 쓰레드들을 notify 해주는 메소드
    public synchronized void setExecutable(boolean on) {
        ready = on;
        notifyAll();
    }
 
    // 가드조건 flag의 상태를 고려하여 실행하는 메소드
    public synchronized void execute() throws InterruptedException, TimeoutException {
        long start = System.currentTimeMillis(); // 시작 시간
        while (!ready) {   // 가드조건 확인
            long now = System.currentTimeMillis(); // 현재 시간
            long rest = timeout - (now - start); // 남은 대기 시간
            if (rest <= 0) {  // 타임아웃 값만큼 다 기다렸다면 익셉션을 발생시켜 balk 함
                throw new TimeoutException("now-start = " + (now - start) + ", timeout = " + timeout);
            }
            wait(rest);    // 남은 대기 시간만큼 기다림
        }
        doExecute();   // 가드조건을 만족하는 경우 처리 수행
    }
 
    private void doExecute() {
        System.out.println(Thread.currentThread().getName() + " calls doExecute");
    }
}


synchronized에는 타임아웃이 없으며, 인터럽트도 불가능하다.

1. synchronized 메소드/블록을 실행하고자 락을 얻기 위해 블록하고 있는 상태

2. wait 메소드를 호출하여 wait set 안에 있는 상태

이 두 가지 상태에 대해 알아보자. 이 둘은 쓰레드가 활동을 멈추고 있다는 점에서 비슷하지만 다른 부분도 있다.


1. synchronized 메소드/블록을 실행하고자 락을 얻기 위해 블록하고 있는 상태

이 상태에 있는 쓰레드를 타임아웃 시킬 방법은 없다. (synchronized 메소드/블록에은 wait 메소드와는 달리 타임아웃을 지정할 수 있는 방법이 없다.)

이 상태에 있는 쓰레드에 대해 인터럽트를 실행하더라도 InterruptedException이 통보되지 않는다. 락을 얻어서 synchronized 안에 뭇히 들어간 후에 wait, sleep, join 등의 인터럽트 상태를 의식하는 메소드를 호출하든지, isInterrupted 메소드나 interrupt 메소드를 사용하여 인터럽트 상태를 조사하고 직접 throw 해야한다.


2. wait 메소드를 호출하여 wait set 안에 있는 상태

Balking 패턴을 통해 살펴보았듯, 이 상태에 있는 쓰레드는 타임아웃 시간 지정이 가능하다. 또한 이 상태에 있는 쓰레드에 대하여 인터럽트를 실행하면 InterruptedException이 통보된다.



java.util.concurrent에서의 타임아웃

Java 5.0의 java.util.concurrent 패키지에는 타임아웃과 관련하여 다음 두 가지 방법이 있다.

1. java.util.concurrent.TimeoutException을 통보하여 타임아웃을 알리는 경우

- java.util.concurrent.Future 인터페이스의 get 메소드

- java.util.concurrent.Exchanger 클래스의 exchange 메소드

- java.util.concurrent.CyclicBarrier 클래스의 await 메소드

- java.util.concurrent.CountDownLatch 클래스의 await 메소드


2. 리턴값으로 타임아웃을 알리는 경우

타임아웃 시간 내에는 몇 번이고 계속 시도하는 경우는, 예외가 아니라 리턴값으로 타임아웃을 알린다.

- java.util.concurrent.BlockingQueue 인터페이스의 offer 메소드에서는 인수로 받은 타임아웃 시간 내에 element를 얻지 못한다면 false를 리턴한다. poll 메소드에서는 null을 리턴한다.

- java.util.concurrent.Semaphore 클래스의 tryAcquire 메소드에서는 false를 리턴한다.

- java.util.concurrent.locks.Lock 인터페이스의 tryLock 메소드에서 false를 리턴한다.


댓글