ひとつのデスクトップにあるイベントは順番に処理されるので、スレッドモデルは簡単です。 デスクトップアプリケーションを開発するように、マルチスレッドと競合を心配する必要はありません。必要なのはイベントリスナを登録し、そして、呼び出されたときにイベントを処理することです。
【ヒント】:イベントリスナはイベント処理スレッドと呼ばれる独立したスレッドの中で実行します。一方、ZUMLページはサーブレットスレッドの中で処理されます。
【ヒント】:イベント処理スレッドを無効にし、すべてのイベントをサーブレットスレッドの中で処理させることができます。これによって、処理速度を速くし、統合問題を少なくします。しかし、処理を一時中断させることはできません。詳しくは上級者のための機能の章のサーブレットスレッドを使用してイベントを処理セクションを参照してください。
アドバンスアプリケーションでは、いくつかの条件を満足するまで処理を一時中断させなければならないことがあります。org.zkoss.zk.ui.ExecutionsクラスのwaitとnotifyとnotifyAllメソッドはこの目的のために作られました。
イベントリスナが自身を一時中断させるにはwaitメソッドを使用します。アプリケーションの条件が満足したら、他のスレッドはnotify又はnotifyAllでそれを呼び起こします。モーダルダイアログはこのメカニズムを使った代表的な例です。
public void doModal() throws InterruptedException {
...Executions.wait(_mutex); //suspend this thread, an event processing thread}
public void endModal() {
...
Executions.notify(_mutex); //resume the suspended event processing thread
}
これらの使用はjava.lang.Objectクラスのwait、notifyとnotigyAllと同じです。しかし、java.lang.Objectクラスのメソッドを使い、イベントリスナを中断や再開できません。さもなければ、関連したデスクトップのすべてのイベント処理はコントロールできなくなります。
Javaオブジェクトのwaitとnotifyとは違って、synchronized ブロックでExecutionsのwaitとnotifyを含むかは自由です。上記のケースでは競合問題にはなりえないので、ブロックを使わなくても結構です。しかし、競合状態になる可能性があれば、Javaオブジェクトのwaitとnotifyを処理するのと同じようにsynchronized ブロックを使います。
//Thread 1
public void request() {
...
synchronized (mutex) {
...//start another thread
Executions.wait(mutex); //wait for its completion
}
}
//Thread 2
public void process() {
... //process it asynchronously
synchronized (mutex) {
Executions.notify(mutex);
}
}
同じデスクトップのイベントは順番に処理されます。つまりイベントハンドラは以下のどのハンドラも遮断します。イベントハンドラが実行するのに時間が長ければ、ユーザーのリクエストを遮断する時間も長くなります。デスクトップアプリケーションのように、ワーキングスレッドを作り、長時間処理の遮断時間を最小にするようにしなければなりません。
HTTPの制限により、以下のルールに従っているかどうかを確認しなければなりません。
ワーキングスレッドを作った後に、Org.zkoss.zk.ui.Executionsクラスのwaitメソッドを使い、イベントハンドラ自体を一時中断させます。
ワーキングスレッドはイベントリスナではないので、コンポーネントにアクセスすることができません。ただし、どのデスクトップにも属していないコンポーネントでしたらアクセスできます。ですので、ワーキングスレッドを開始する前に、必要な情報を手動で追加する必要があります。
そして、ワーキングスレッドは必要に応じて、情報を削除したり、コンポーネントを作成したりします。ただし、デスクトップに属しているコンポーネントを参照しないことに注意しましょう。
ワーキングスレッドが終了したら、その中にあるorg.zkoss.zk.ui.Executionsクラスの中のNotify(Desktop desktop, Object flag)又はnotifyAll(Desktop desktop, Object flag)メソッドを使って、イベントハンドラを再開します。
他のイベントがクライアントから送られてくるまで、再開されたイベントハンドラは実行されません。強制的にイベントを送信させるには、タイマーコンポーネント(org.zkoss.zul.Timer)を使い、イベントを少し後にまたは周期的に起こします。このタイマーのイベントリスナは何もしないか、状態の更新を行います。
ラベルを非同期的に生成することを想定します。もちろん、マルチスレッドでそのような簡単なタスクをするのは効率的ではありませんが、この例を参考にして、複雑なタスクで使用してください。
//WorkingThread
package test;
public class WorkingThread extends Thread {
private static int _cnt;
private Desktop _desktop;
private Label _label;
private final Object _mutex = new Integer(0);
/** Called by thread.zul to create a label asynchronously.
* To create a label, it start a thread, and wait for its completion.
*/
public static final Label asyncCreate(Desktop desktop)
throws InterruptedException {
final WorkingThread worker = new WorkingThread(desktop);
synchronized (worker._mutex) { //to avoid racing
worker.start();
Executions.wait(worker._mutex);
return worker._label;
}
}
public WorkingThread(Desktop desktop) {
_desktop = desktop;
}
public void run() {
_label = new Label("Execute "+ ++_cnt);
synchronized (_mutex) { //to avoid racing
Executions.notify(_desktop, _mutex);
}
}
}
次に、onClickのようなイベントリスナでワーキングスレッドを呼び出すZUMLページを作成します。
<window id="main" title="Working Thread">
<button label="Start Working Thread">
<attribute name="onClick">
timer.start();
Label label = test.WorkingThread.asyncCreate(desktop);
main.appendChild(label);
timer.stop()
</attribute>
</button>
<timer id="timer" running="false" delay="1000" repeats="true"/>
</window>
中断されているイベントリスナ(onClick)を再開するにはタイマーを使わなければなりません。それを不自然だと感じるかもしれませんが、それはHTTPの制限によるものです:ブラウザでページを存続させるには、イベント処理が一時中断されているときでもレスポンスをしなければいけません。その結果、ワーキングスレッドが役割を果たしてイベントリスナに通知したとき、HTTPのリクエストはすでになくなっています。それを解決するため、タイマーを使用します。
より正しくいうと、ワーキングスレッドがイベントリスナへ再開を通知するとき、ZKは順番待ちのリストに再開通知を追加するだけです。他のHTTPリクエストを受け取ってからリスナが本当に再開されます。(上の例ではそれがonTimerイベントです。)
この単純な例の中で、onTimerイベントを操作しませんでした。複雑なアプリケーションを処理する場合、onTimerで処理のステータスを送信することもできます。
中断と再開なしで長時間処理を実装することは可能です。同期コードが複雑になる場合にとても便利です。
そのアイデアは単純です。ワーキングスレッドはある場所に結果を一時的に保存します。そして、onTimerイベントリスナは結果をデスクトップへ表示させます。
//WorkingThread2
package test;
public class WorkingThread2 extends Thread {
private static int _cnt;
private final Desktop _desktop;
private final List _result;
public WorkingThread2(Desktop desktop, List result) {
_desktop = desktop;
_result = result;
} public void run() {
_result.add(new Label("Execute "+ ++_cnt));
}
}
そして、onTimerイベントリスナの中にラベルを付加します。
<window id="main" title="Working Thread2">
<zscript>
int numPending = 0;
List result = Collections.synchronizedList(new LinkedList());
</zscript>
<button label="Start Working Thread">
<attribute name="onClick">
++numPending; timer.start;
new test.WorkingThread2(desktop, result).start();
</attribute>
</button>
<timer id="timer" running="false" delay="1000" repeats="true">
<attribute name="onTimer">
while (!result.isEmpty()) {
main.appendChild(result.remove(0));
--numPending;
}
if (numPending == 0) timer.stop();
</attribute>
</timer>
</window>周期的に結果を検査する代わりに、例えば、ユーザーがボタンをクリックしたり何かを入力したりするときに便乗し、クライアントへ送信することができます。
それをするには、ルートコンポーネントのonPiggybackイベントにイベントリスナを登録します。そうすると、ZK Update Engine がイベントを処理するたびにリスナが呼び出されます。例えば、以下のようにコードを書き換えることができます。
<window id="main" title="Working Thread3" onPiggyback="checkResult()">
<zscript>
List result = Collections.synchronizedList(new LinkedList());
void checkResult() {
while (!result.isEmpty())
main.appendChild(result.remove(0));
}
</zscript>
<button label="Start Working Thread">
<attribute name="onClick">
timer.start();
new test.WorkingThread2(desktop, result).start();
</attribute>
</button>
</window>
Piggybackのいいところは、クライアントとサーバー間の通信パケットを増やさないことです。しかし、この方法だと、クリックや入力などのユーザーの操作がなければ、ページは更新されません。ですので、Piggybackを適用できるかどうかはそのアプリケーションの要求しだいです。
【補足】:可延イベントはすぐにクライアントに送られることはありませんので、不可延のイベントが起こったときにのみonPiggybackイベントは発生します。可延イベントリスナセクションを参照してください。