倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

はじめての幻獣 Griffon 研 (16) : コストのかかる計算を別スレッドで(理論編) edt / doLater / doOutside

前回、「関数描画アプリケーション」で Charts プラグインを使ってグラフを描画するようにしました。 ついでにサンプル数も変更できるようにしました。 で、調子に乗ってサンプル数を 10,000 とか 100,000 とかにしてグラフを描かせていると見事にフリーズ・・・ ってことで、これを改善すべく、今まで敢えて(?)目を背けてたスレッド関連の話にそろそろ足を踏み入れてみようかと*1一覧)。

さて、「Griffon Guide」のスレッドの部分を見ていくと、「長い計算は Event Dispach Thread (EDT) の外で行うべし」と書かれてます・・・ つーか「EDT」ってナニ? まず Swing のスレッドの扱いから復習した方がよさそうダネ。

Swing でのスレッド


えーと、「Griffon Guide」から張られているリンク「Concurrency in Swing」は断空されているので、Oracle のサイトからそれっぽいのを検索。 The Java Tutorials 「Concurrency in Swing」ってページがあったのでこれでしょう。 ここを見ると、Swing では次の3種類のスレッドがあると書かれています:

Initial Threads
アプリケーションが開始されるスレッド。 Java プログラムの main() メソッドを実行するスレッドでもあるそうです。
Event Dispatch Thread (EDT)
全てのイベントを処理するコードが実行されるスレッド。 Swing フレームワークと相互作用するコードはこのスレッド内で実行されなければいけません。
Background Threads
時間のかかる処理を実行するスレッド。

Griffon では Initial Threads を気にする必要はないでしょう(たぶん)。 Background Threadsjavax.swing.SwingWorker クラスを用いて処理が実装されるためか Worker Threads とも呼ばれるようです。 ただし、Griffon では(少なくとも直接は)SwingWorker を使わないので、以降この名前は使いません。

ってことで、Griffon で大切なのはとりあえず EDT のようですね。 EDT のドキュメントは1ページのみありますが()、「Griffon Guide」のスレッドの箇所の方が分かりやすいのでそちらへいきましょう。

Griffon でのスレッド・プログラミング


Griffon Guide 「9. Threading」を読むと、大切なのは次の黄金律だそうです:

all long computations must be performed outside of the Event Dispatch Thread (or EDT for short).
全ての長い計算は EDT の外で行うべし

EDT の外」っていうのは Swing の Background Threads ですね。 この黄金律は逆に言えば次のようなルールにもなるようです:

all interaction with UI components must be done inside the EDT, including building a component and reading/writing component properties.
全てのUI コンポーネントとの相互作用(コンポーネントの構築、コンポーネントのプロパティの読み書きを含む)は EDT の中で行われるべし

さてこれらを踏まえて、Griffon では以下のようなメソッドが使用できます(通常、これらを使用するのは Controller 内部でしょう)。 Griffon 0.9.5 以降を使う場合は「追記 その1」参照

EDT 内/外 同期/非同期 Swing Swing 以外
EDT 内 同期
非同期
edt
doLater
execSync
execAsync
EDT 外 - doOutside execOutside

これらはすべてクロージャを引数にとり、その内容を上記の方法で実行します。 ちなみに、Swing 以外の UI toolkit では、EDT のことを UI Thread と呼ぶようで*2、EDT 内外は UI Thread 内外となります。

サンプルコード

Griffon 内で上記のメソッドを使用する方法は、概ね以下のようになります(Swing を使用する場合)。 Griffon 0.9.2 以降を使う場合は「追記 その2」を参照

class MyController {

    def model

    def action = { event = null ->
        // (1) デフォルトでは EDT 内
        def value = model.value    // (2) model などから値を取得

        doOutside {    // EDT 外で処理

            // (3) コストのかかる処理

            doLater {    // EDT 内で処理

                model.result = …    // (4) model などへ値を設定
            }
        }
    }
}
  • (1) Controller に記述したイベント処理は、大抵の場合 EDT 内で処理が始まります(むしろそうすべき。)
  • (2), (4) model のプロパティは通常 View とバインドされるため、model のプロパティの読み書きは EDT 内で行う必要があります。 まぁ、view のプロパティは言うまでもなく EDT 内で読み書きする必要があるでしょうね。
  • (3) コストのかかる計算は EDT 外で行います(doOutside)。 この計算に model のプロパティなどを使用したい場合は、(2) のように EDT 内で値をローカル変数にコピー*3してから、それを doOutside 内で使用します。

まぁ、基礎固めはこんなところで。

関数描画アプリケーションへの適用


は次回に。

追記 その1


Griffon 0.9.5 から、Swing 以外での各種実行メソッドの名前が変更になってます:

EDT 内/外 同期/非同期 Swing Swing 以外
EDT 内 同期
非同期
edt
doLater
execInsideUISync
execInsideUIAsync
EDT 外 - doOutside execOutsideUI

追記 その2


Griffon 0.9.2 以降では、アクションを定義するクロージャがデフォルトでは EDT 外で実行されるように変更されました。 したがって、上記のコードは以下のようになります:

class MyController {

    def model

    def action = { event = null ->
        // (1) 0.9.2 以降、デフォルトでは EDT 外
        def value = null
        edt{
            value = model.value    // (2) model などから値を取得
        }

        // (3) コストのかかる処理

        doLater {    // EDT 内で処理
                model.result = …    // (4) model などへ値を設定
        }
    }
}

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

*1:サンプル数 1000,000 くらいにすると「メモリ不足」って言われたので、これはスレッドでは解決できませんねぇ。

*2:単に呼び方が違うだけなのかどうかは不明。

*3:Immutable オブジェクトや Value オブジェクトでない場合は単に参照をコピーするだけじゃまずいでしょうね。