multi thread 11, 12

§11. Thread-Specific Storage パターン

シングルスレッドの環境で動作することを想定しているオブジェクトをマルチスレッドで利用したいとする.この場合,スレッド固有の記憶領域を確保することになる.もともとスレッドはメソッドの局所変数を保持しているスタックとして,固有の領域を持っている.その局所変数はそのスレッド固有のものであり,他のスレッドからアクセスされることはないが,メソッド呼び出しが終了すれば消えてしまう.

そこで,java.lang.ThreadLocalに格納していく.このインスタンスはコレクションの一種で,メソッド呼び出しとは無関係にスレッド固有の領域を確保するためのクラス.

つまりスレッド固有の情報を置く場所は次の二つがある

  • スレッド外(thread-external)
    ThreadLocalのインスタンスに格納する場合.スレッドを表す既存のクラスを修正する必要がなく,任意のスレッドに適用可能.その代わり,スレッドを表すクラスのソースが理解しにくくなる危険性がある.
  • スレッド内(thread-internal)
    Threadクラスのサブクラス内でフィールド宣言をすればスレッド固有の情報になる.スレッドのソースを読めばスレッド固有の情報が何か分かりやすくなる.その代わり,後からスレッド固有の情報を追加するときはこのサブクラスを書き換える必要がある.

ちなみに,スレッドとスレッドが利用する情報の関係について,アクター・ベースとタスク・ベースという考えがある.

  • アクター・ベース
    「スレッド」が主体.スレッドを表すインスタンスに対して,仕事を行うための情報を持たせるもの.そうするとスレッド同士がやり取りする情報を小さく・軽くすることができる.
    各スレッドは他のスレッドから受け取った情報を使って処理を行い,自分の内部情報を変化せる.

    class Actor extends Thread {
        (The actor's internal state)
        public void run() {
            (Recieving the external task from another thread, the actor's internal state is changed.)
        }
    }
  • タスク・ベース
    「タスク」が主体.スレッドに情報を持たせないもの.スレッドではなく,スレッド同士がやり取りするインスタンス(「タスク」)の方に情報を持たせる.データを持たせるだけではなく,実行するためのメソッドまで持たせる.なので,実行するのはどのスレッドでも構わない.

    class Task implements Runnable {
        (Information to run the task)
        public void run() {
            (The concrete execution)
        }
    }

§12. Active Object パターン

処理を要求するスレッド(Client)と,処理の内容を記述したオブジェクト(Servant)がいるとする.Servantはシングルスレッドでの利用を想定する.

こんな状況を考える.複数のClientからServantを利用したいけれど,Servantはスレッドセーフではない.Servantの処理に時間がかかる場合でもClientへの応答性を落としたくない.処理の要求順序と実行順序は一致しない.処理の結果はClientへ返す.

これを解決するには

  1. 非同期メッセージを受け取り,Clientとは独立のスレッドを持つ能動的なオブジェクトを構築する.
  2. Schedulerスレッドを1個導入する.
  3. Servantの呼び出しはSchedulerだけが行う.(Servantをマルチスレッド対応せずに複数のClientが処理できる.Worker Threadパターン,Wrapperとも)
  4. Clientからの要求はProxyへのメソッド呼び出しとする.
  5. Proxyはその要求を1つのオブジェクトに変換し,Producer-ConsumerパターンでSchedulerへ渡す(Clientへの応答性を保つ)
  6. Schedulerが実行すべき要求を選び出して実行する.(実行順序はSchedulerが決定)
  7. 実行結果はFutureとしてClientへ返す.
広告

multi thread 10

Two-Phase Termination パターン

二段階の終了.きちんと終了処理をして実行を終える,gracefulな終了を行う.

スレッドの作業中に別のスレッドから終了要求を受け取ったこのスレッドは,いきなり終了せず必要な後片付けを始め,終了処理中状態になる.そして終了処理が完了すると,今度こそ本当にスレッドが終了する.

public class Terminator extends Threads {
    private volatile boolean shutdownRequested = false;
    public void shutdownRequest() {
        shutdownRequested = true;
        interrupt();
    }
    public boolean isShutdownRequested() {
        return shutdownRequested;
    }
    public final void run() {
        try {
            while (!isShutdownRequested()) {
                doWork();
            }
        } catch (InterruptedException e) {
        } finally {
            isShutdown();
        }
    }
}
  • Threadクラスのstopは使わない(インスタンスの安全性が失われる)
  • フラグのテストだけでは不十分(interruptすることで,そのスレッドがsleepしていた場合も中断させることができる)
  • インタラプト状態のテストだけでも不十分(InterruptedExceptionが投げられた場合はインタラプト状態でなくなる→shutdownRequestが出されたことが分からなくなる)

java.util.concurrent.ExecutorServiceには

  • isShutdown
  • isTerminated

が含まれており,その戻り値は

isShutdown isTerminated
作業中 false false
終了処理中 true false
終了 true true

ちなみに,interruptメソッドを呼び出すと,スレッドにインタラプトをかけることができるが,この結果は次の

  1. スレッドが「インタラプト状態」になる(状態への反映)
  2. 例外InterruptedExceptionが投げられる(制御への反映)

のいずれかとなる.通常は(1)で,スレッドがsleep, wait, join していた場合には(2)になる.これを変換することを考える.

  1. インタラプト状態→例外InterruptedException
    if (Thread.interrupted()) { // interrupt state of Thread.currentThread()
        throw new InterruptedException();
    }
  2. 例外InterruptedException→インタラプト状態
    try {
        (Long time execution)
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

multi thread 9

Future (先物)パターン

返り値を得るまで時間のかかる処理をFutureパターンで実装すると,依頼したクラスは直ちに「FutureData」を返り値として受け取る.これは受け取った時点では値がないが,しばらくして処理が終わったころにアクセスしてみると,実際のデータ「RealData」が返ってくるようなものである.いわば,引換券のようなもの.

  • Host class
    public class Host {
        public Data request(args) {
            // (1) create a new FutureData
            final FutureData future = new FutureData();
            // (2) Invocation for creating a new RealData
            new Thread() {
                public void run() {
                    RealData realdata = new RealData(args);
                    future.setRealData(realdata);
                }
            }.start();
            // (3) return the instance of FutureData
            return future;
        }
    
    }
    1. FutureDataのインスタンスを作る
      リクエストを受け取ると作り始める
    2. RealDataのインスタンスを作るための新しいスレッドを起動する
      新しいスレッドの中でRealDataのインスタンスを作るようにする
    3. FutureDataのインスタンスを戻り値とする
      リクエストを呼び出した(メイン)スレッドはfutureを戻り値として直ちに帰る.
  • Data IF
    抽象メソッド getContent() を表現したインターフェース.FutureDataとRealDataが実装する
  • FutureData class
    引換券となるクラス.readyはrealdetaに値が代入されたかどうかを表すフィールド.
    setRealDataではRealDataのインスタンスをrealdataに代入する.RealDataのインスタンスができたら,readyをtrueにし,getContentの中で待っているスレッドを起こすためにnotifyAllする.一方,2回以上RealDataのインスタンスを生成するのはリソースの無駄なのでBalkingパターンでこれを防いでいる.
    getContentは実際のデータを得るためのメソッド.readyをガード条件としてGuarded Suspensionパターンでrealdataがセットされるのを待っている.

    public class FutureData implements Data {
        private RealData realdata = null;
        private boolean ready = false;
        public synchronized void setRealData(RealData realdata) {
            if (ready) {
                return;
            }
            this.realdata = realdata;
            this.ready = true;
            notifyAll();
        }
        public synchronized String getContent() {
            while (!ready) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
            return realdata.getContent();
        }
    }
  • RealData class
    RealDataクラスはインスタンスを作るのに時間がかかるクラス.このクラスには「インスタンスができるまで待つ」というようなスレッド制御は含まれていない.マルチスレッドのことは何も考えなくて良い.

    public class RealData implements Data {
        private final String content;
        public RealData (args) {
            (consume a lot of time)
        }
        public String getContent() {
            return content;
        }
    }

Futureはコールバックよりもマルチスレッドにおいて安全.
コールバックというのは,処理が完了したときにHost役(新規スレッドを作り起動する)が起動したスレッドがClient役(タスクを依頼した)のメソッドを呼ぶ.ただし,マルチスレッドではこのClient役の方でもマルチスレッドを意識しないといけない.

java.util.concurrentパッケージに含まれているクラスとインターフェースも見ておくと

  • java.util.concurrent.Callable IF
    戻り値のある処理の呼び出しを抽象化している.callメソッドが宣言されている.Callable<String> は「callメソッドの戻り値の型がStringであるCallableIF」を表す
  • java.util.concurrent.Future IF
    Futureを抽象化している.値を取得するget, 実行を中断するcancelが宣言されている.値をセットするセッターメソッドは実装クラスで実装する必要がある.
  • java.util.concurrent.FutureTask Class
    FutureIFを実装した標準的なクラス.値を設定するset,例外を設定するsetExceptionメソッドが宣言されている.また,FutureTaskはRunnableIFも実装しているので,runメソッドも実装されている.

CallableIFを使うと,Host Classは

public class Host {
    public Data request(args) {
        // (1) create a new FutureData
        final FutureData future = new FutureData(
            new Callable<RealData>() {
                public RealData call() {
                    return new RealData(args);
                }
            }
        );
        // (2) Invocation for creating a new RealData
        new Thread(future).start();
        // (3) return the instance of FutureData
        return future;
    }

}

次に,FutureTaskを使うと,FutureDataクラスは

public class FutureData extends FutureTask<RealData> implements Data {
    public FutureData(Callable<RealData> callable) {
        super(callable);
    }
    public String getContent() {
        String string = null;
        try {
            string = get().getContent();  // (1)
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
        return string; // (2)
    }
}

コンストラクタで与えられたCallableオブジェクトは,そのままスーパークラスのFutureTaskに渡す.callメソッドの呼び出しはFutureTaskクラスにお任せする.get()はFutureTaskのメソッドで戻り値はFutureTask<RealData>としたので,RealDataが返ってくる.なので,getContent()することができ(1),これをDataの実装としてgetContent()の返り値(2)としている.