Home   Single Page

長時間処理

同じデスクトップのイベントは順番に処理されます。つまりイベントハンドラは以下のどのハンドラも遮断します。イベントハンドラが実行するのに時間 が長ければ、ユーザーのリクエストを遮断する時間も長くなります。最悪、ユーザーはHTTPの制限により、ブラウザの左片隅に見える小さなプロセスダイアログによる他は何のヒントも得ることは出来ません。

上記のエコーイベントのセクションで説明したように、エコーイベントと showBusy メソッドにより、 長時間処理において、より的確なメッセージを提供することでユーザーが、言ってみれば関係ないボタンを押すなどして更にパフォーマンスを落とすことを防ぐことができます。

しかし、アプリケーションによっては、ユーザーのアクセスをブロックすることが容認できない場合があります。デスクトップアプリケーションのように、長時間処理のプロセスでブロッキングを防ぐには、他のワーキングスレッドで動作させる必要があります。それからクライアントに処理ステータスをリポートします。

ZK には 4 つの選択肢があります。サーバープッシュ、サスペンドとレジューム、タイマー、そしてピギーバックです。

対処法 1: サーバープッシュ

サーバープッシュはいわゆるリバース-Ajax 技術で、サーバーがクライアントにアクティブにコンテンツを送信するものです。サーバープッシュの助けによって、前もって準備した条件が満足されたとき、ワーキングスレッド内でコンテンツをクライアントに送信したり、更新したりできます。サーバープッシュの使用法は簡単で、以下の 3 つの手順を踏むだけです。

  1. デスクトップのサーバープッシュをイネーブルにする。

    サーバープッシュをイネーブルにするには、Desktop.enableServerPush(boolean bool)を起動する。

  2. ワーキングスレッドに更新を要するコンポーネントを渡す。

  3. デスクトップ内でワーキングスレッドを起動する。

ノート: サーバープッシュを使うには、 zkex.jar か zkmax.jar をインストールします。でなければ自分で org.zkoss.zk.ui.sys.ServerPush を実装する必要があります。

<window title="Demo of Server Push">
<zscript>
    import test.WorkingThread;    
    WorkingThread thread;    
    void startServerpush(){    
        //enable server push        
        desktop.enableServerPush(true);        
            //invoke working thread and passing required component as parameter            
            thread= new WorkingThread(info);            
        thread.start();        
    }    
    void stopServerpush(){    
        //stop the thread        
        thread.setDone();        
        //disable server push        
        desktop.enableServerPush(false);        
    }    
</zscript>
    <vbox>    
        <button label="Start Server Push" onClick="startServerpush()"/>        
        <button label="Stop Server Push" onClick="stopServerpush()"/>        
    <label id="info"/>    
    </vbox>    
</window>

セキュリティ イシュー

注目すべきことのひとつは、デスクトップが同時に複数のスレッドからアクセスされた場合に起こる同期の問題です。この問題を避けるためには、以下のようにデスクトップにアクセスする前に Executions.activate(Desktop desktop) を起動してフルアクセスコントロールを獲得して、スレッドが仕事を完了した後に Executions.deactivate(Desktop desktop) を起動して コントロールを開放します。

    packagetest;    

        publicclassWorkingThread extends Thread{        
    private final Desktop _desktop;    
    private final Label _info;    
    private int _cnt;    
    private boolean _ceased;    
        publicWorkingThread(Label info){        
        _desktop = info.getDesktop();        
            _info= info;            
                            }publicvoidrun(){try{                            
                while(!_ceased){                
                Threads.sleep(2000); //Update each two seconds                
                Executions.activate(_desktop); //get full control of desktop                
                    try{                    
                    _info.setValue(Integer.toString(++_cnt));                    
                                            }catch(RuntimeException ex){throw ex;                                            
                    }catch(Error ex){                    
                    throw ex;                    
                }finally{                
                    Executions.deactivate(_desktop);                    
                        //release full control of desktop                        
                }                
            }            
            }catch(InterruptedException ex){            
            }            
                }publicvoidsetDone(){                
        _ceased = true;        
    }}    
背景
Behind the Scene

サーバープッシュのメカニズムはクライアント-ポーリングのメカニズムを使用して実装されていますので、ワーキングスレッドの仕事を起動するにはクライアントが周期的にサーバーに問いかけます。問いかけの周期はExecutions.setDelay(int min, int max, int factor) を起動することで手動で修正できます。

  • min, サーバーにペンディングしているサーバープッシュスレッドについて問いかける、最低遅延時間

  • max, サーバーにペンディングしているサーバープッシュスレッドについて問いかける、最大遅延時間

  • factor, 処理中の実遅延にこの factor を掛け算します。

最後の注目事項は、サーバーの負荷に応じて、自動的にこの周期が調整されることです。

対処法 2: スレッド サスペンド と レジューム

サーバープッシュの助けでマルチスレッド問題を気にしなくても良くなりました。しかし、この仕事を自分で扱おうとすれば、 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で処理中のステータスを送信することもできます。

対処法 3: タイマー(再開/中断なし)

中断と再開なしで長時間処理を実装することは可能です。同期コードが複雑になる場合にとても便利です。

そのアイデアは単純です。ワーキングスレッドはある場所に結果を一時的に保存します。そして、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>

対処法 4: ピギーバック(中断、再開、タイマーなし)

周期的に結果を検査する代わりに、例えば、ユーザーがボタンをクリックしたり何かを入力したりするときに便乗し、クライアントへ送信することができます。

それをするには、ルートコンポーネントの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イベントは発生します。可延イベントリスナセクションを参照してください。