Threadの割り込みハンドラが失敗することがある(その1)

[追記]この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


Threadを使っているとある問題に気づく事があります。
それは「イベントをリスンするThreadでinterrupt()を呼び出した際に割り込みハンドラが実行されないことがある」問題です。
event()を実行してイベントのリスンを開始したThreadは待機状態となり、その状態でinterrupt()を実行すると割り当てられた割り込みハンドラが実行されます。が、場合によっては期待する割り込みハンドラが実行されない事があるのです。

上記の問題は、サイクルとサイクルの狭間、つまり一つのThreadの実行関数が終わり次のThreadの実行関数が始まるまでの間に、イベントハンドラとinterrupt()の両方が実行された時におこります。

(※ここでいうサイクルとはThread.initialize()で渡すIThreadExecutorの実装によって定義されるThreadの実行の単位のことです。例えばEnterFrameThreadExecutorはユーザが定義したフレームレート、30fpsなら1/30秒に一回Threadが実行され、IntervalThreadExecutorならIntervalThreadExecutorのコンストラクタに渡したインターバルで一回Threadが実行されます。)

この問題に気づく典型的な例は、マウスクリックに反応するようにイベントをリスンしているThreadをinterrupt()するケースやステージのリサイズを監視するThreadをinterrupt()するケースなど、短い間隔でイベントをキャッチするThreadに割り込むケースです。
(参考:Threadの割り込みがうまくいかないことがある

このエントリはここから普段ユーザが意識する必要のないThread内部の振る舞いを見ていきます。
もしこの問題の解決方法だけを知りたい場合は「Threadの割り込みハンドラが失敗することがある(その2)」を見てみてください。

問題が再現するテストを書く

さて、この問題が再現するテストを書いてみて何がおきているかを調べてみます。
以下がそのテストコードです。ThreadライブラリのリポジトリにあるtestディレクトリにTestSuiteが用意されているので、ここに今回のテストInterruptHandlerTest.asを追加しました。

package org.libspark.thread

{

    import flash.utils.setTimeout;

    import flash.display.Sprite;

    import org.libspark.as3unit.after;

    import org.libspark.as3unit.assert.*;

    import org.libspark.as3unit.before;

    import org.libspark.as3unit.test;

    

    use namespace test;

    use namespace before;

    use namespace after;

    /**

     * 割り込みハンドラのテスト

     *   イベントをリスンするThreadインスタンスがあり、

     *   Threadの実行タイミング間でイベントの配信と割り込みが両方行われた時に

     *   割り込みハンドラが正しく実行されるかテスト

     */

    internal class InterruptHandlerTest extends Sprite

    {

        private var dispatcher : Dispatcher;

        private var thread : InterruptHandlerTestThread;

        before function setup() : void

        {

            Thread.initialize(new EnterFrameThreadExecutor());

            Static.log = “”;

            

            //イベント配信オブジェクト

            dispatcher = new Dispatcher();

            

            //テスト用Thread

            thread

                new InterruptHandlerTestThread(dispatcher);

        }

        after function teardown() : void

        {

            Thread.initialize(null);

            Static.log = “”;

        }

        //イベント配信直後に割り込みするケースのテスト

        test function inturruptHandlerExecution() : void

        {

            //200mSec後に [イベント配信] -> [割り込み]

            setTimeout(

                function():void

                {

                    //Threadに向けてイベントを配信

                    dispatcher.dispatch();

                    

                    //Threadinterrupt()を呼び出し

                    trace(“¥n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!¥n”);

                    thread.interrupt();

                    

                }, 200

            );

            //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

            setTimeout(

                function():void

                {

                    dispatcher.dispatch();

                },

                250

            );

            

            //300mSec後にテスト

            setTimeout(

                async(

                    function():void

                    {

                        //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                        assertEquals(“event interrupt interrutHandler finalize “, Static.log);

                        //割り込み後のステータスをテスト

                        assertEquals(ThreadState.TERMINATED, thread.state);

                    }

                ), 

                300

            );

                

            //テスト用Thread開始

            thread.start();

        }

    }

}

import org.libspark.thread.Thread;

import flash.events.Event;

import flash.events.EventDispatcher;

//テスト用Thread

class InterruptHandlerTestThread extends Thread 

{

    private var dispatcher : Dispatcher;

    

    public function InterruptHandlerTestThread(dispatcher:Dispatcher)

    {

        super();

        this.dispatcher = dispatcher;

    }

    

    override public function interrupt():void

    {

        Static.log += “interrupt “;

        super.interrupt();

    }

    

    override protected function run() : void

    {

        trace(”    # run()”, printStatus(state));

        

         //割り込みハンドラを生成

        var interruptHandler : Function

            function():void

            {

                trace(”    # INTERRUPTED¥n”);

                Static.log += “interrutHandler “;

            };

        

        //割り込みハンドラを設定

        interrupted(interruptHandler);

        

        //イベントをキャッチしたら再びイベントをリスンして待機状態へ

        event(

            dispatcher,

            InternalEvent.COMPLETE,

            function():void

            {

                trace(”    # received event”, printStatus(state));

                Static.log += “event “;

                //イベントを受け取ったら再びイベントをリスン

                next(run); 

            }

        );

    }

    override protected function finalize() : void

    {

        Static.log += “finalize “;

        trace(”    # finalize()”, printStatus(state));

    }

    

}

//イベント配信オブジェクト

class Dispatcher extends EventDispatcher

{

    public function dispatch():void

    {

        trace(“¥n [DISPATCH]¥n”);

        dispatchEvent(new InternalEvent(InternalEvent.COMPLETE));

    }

}

//テスト用イベント

class InternalEvent extends Event

{

    public static const COMPLETE : String = “COMPLETE”;

    

    public function InternalEvent(type : String)     

    {

        super(type, false, false);

    }

}

class Static

{

    public static var log:String;

}

さらに、Thread内の動きを観察するために、org.libspark.thread.Threadとorg.libspark.thread.EnterFrameThreadExecutorにtrace()を仕込みます。

Threadにデバッグ用メソッドを追加

//Threadのステータスコードを文字列に変換するユーティリティ

public static function printStatus(s : int) : String

{

    switch(s)

    {

        case ThreadState.NEW:

            return “(NEW”;

        case ThreadState.RUNNABLE:

            return “(RUNNABLE”;

        case ThreadState.TERMINATED:

            return “(TERMINATED”;

        case ThreadState.TERMINATING:

            return “(TERMINATING”;

        case ThreadState.TIMED_WAITING:

            return “(TIMED_WAITING”;

        case ThreadState.WAITING:

            return “(WAITING”;

    }

    throw new Error(s);

}

Threadの以下のメソッドにtrace()を追加
event()メソッド

public static function event(dispatcher:IEventDispatcher, type:String, func:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void

        {

            trace(”  @ register event “, printStatus(getCurrentThread().state));

            getCurrentThread().addEventHandler(dispatcher, type, func, useCapture, priority, useWeakReference);

eventHandler()メソッド

private function eventHandler(e:Event, handler:EventHandler):void

        {

            trace(”  @ eventHandler “, printStatus(_state));

            //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

interrupt()メソッド

public function interrupt():void

        {

            trace(”  @ interrupt “, printStatus(_state));

            //割り込みを予約し、次のサイクルの最後に実行する

internalExecute()メソッド

private function internalExecute(error:Object, errorThread:Thread):Boolean

        {

            trace(”  @ internalExecute “ + [printStatus(_state)]);

            if (_state == ThreadState.WAITING || _state == ThreadState.TIMED_WAITING) {

EnterFrameThreadExecutorのenterFrameHandlerにtrace()を仕込みます。(カウンタ用の変数を一つ追加しています)

private var time : int = 0;

private function enterFrameHandler(e:Event):void

{

trace(“¥nCYCLE “ + time++ + “————–“);

Thread.executeAllThreads();

}

これで準備完了。
このテストでは、テスト開始100mSec後にイベント配信とThreadの割り込みを行い、その50mSec後にもう一度イベント配信、最後に200mSec後に割り込みハンドラが実行されたか確認しています。


テスト

テストしてみます。
割り込みハンドラが実行されずにテストは失敗します。

以下はその出力結果です。

CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
〜〜 省略 〜〜
CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
CYCLE 10--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
CYCLE 11--------------
@ internalExecute (WAITING
.E
Time: 0.341
There was 1 failure:
1) inturruptHandlerExecution(org.libspark.thread::InterruptHandlerTest)
Error: expected:<event interrupt [interrutHandler finalize] > but was:<event interrupt [event] >

何が起きたか見ていきましょう。

@マークのある行はThread(スーパークラス)内部でおこった出来事、#マークのある行はThread(テスト用のサブクラス)で起こった出来事です。
マークの後に何が起こったか、そしてその時のThreadのステータスを出力しています。
例えば、

@ internalExecute (RUNNABLE

はThread.internalExecuteが実行され、その時のステータスはRUNNABLEだったという感じです。
(※このテストコードの出力結果は実行時のフレームレートやCPUの速度などに影響します。もしあなたがこのテストを実行した場合、テストの成否は同じはずですが出力結果は異なる可能性があります。今回は31FPSでこのテストを実行しました。)

まず、CYCLE0を見てみてください。

CYCLE 0--------------
@ internalExecute (RUNNABLE
# run (RUNNABLE
@ register event  (RUNNABLE

run()が実行されイベントハンドラが設定されています。
ここでこのサイクルは終了です。

次に、CYCLE8を見てみます。
このサイクルで割り込みハンドラが実行されない問題が起きています。

CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event  (RUNNABLE

まずイベントが配信されイベントハンドラが実行されるという期待した動作がスムーズに展開していきます…

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE

その後、interrupt()が実行されますが、次のサイクル9で実行されるはずの割り込みハンドラは実行されていません。
また、割り込まれた後イベントのリスンをやめる事が期待されていますが、次のサイクルであるサイクル9でも何事もなかったようにイベントをリスンし続けています。

何が起きたのでしょうか?

もう一度割り込まれた時の出力結果を見てみます。

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt (RUNNABLE

interrupt()が呼び出されたときThreadのステータスはRUNNABLEになっています。
これは、interrupt()される直前にイベントハンドラが実行され、ステータスが変更されたためです。


Threadの仕様によると、割り込みハンドラが実行される条件はイベントハンドラが設定されている事、つまりステータスがWAITINGであることです。しかし割り込み直前でイベントハンドラが実行されたためにステータスがRUNNABLEになってしまいました。イベントハンドラが再度設定されステータスが再びWAITINGになるのは次のサイクルです。次のサイクルが始まる前に割り込みが実行されると、この時点ではまだステータスがRUNNABLEであるため、このエントリで取り上げている問題が起こります。

テキストだけだとわかりにくいので図にしてみました。(前のエントリでも書いてみたシーケンス図です。見方がわからない場合は前のエントリを参照してください)

 

まず、割り込みハンドラが意図する通りに実行されるケースです。

successfuly interruption

つぎに失敗したケースです。

failed interruption

サイクルとサイクルの間でイベントハンドラの実行と割り込みの実行の両方が起きているのがわかります。
イベントのキャッチによりThreadのステータスが一時的にRUNNABLEとなり、そのタイミングで割り込みが実行されると割り込みハンドラは失敗します。

「割り込みハンドラが実行されるのは待機状態に割り込まれた時」とThreadは仕様としているので、これは正しい動作と言えなくもないです。が、ユーザは(このテストのようなケースでは)「イベントを受け取ったら再びイベント待機状態」つまり「割り込まれるまでは常に待機状態」のThreadを意図していると思うのでバグのようにも思えます。
この問題が仕様なのかバグなのか微妙なところです。

運用でカバーできないのか?

ステータスが一時的にRUNNABLEになってしまうのが問題です。しかしステータスがRUNNABLEの時に割り込みが実行されるとすれば割り込みフラグ(isInterrupted)たつはずです。このフラグを利用すれば手動で割り込みハンドラを呼び出す事ができるのではないでしょうか?
これを使って運用でカバーできないのでしょうか?

例えば以下のようなコードです。

override protected function run() : void

{

    trace(”    # run()”, printStatus(state));

    

     //割り込みハンドラを生成

    var interruptHandler : Function

        function():void

        {

            trace(”    # INTERRUPTED¥n”);

            Static.log += “interrutHandler “;

        };

        

    //割り込みフラグがたっていれば割り込みハンドラを実行して終了

    if (isInterrupted)

    {

        interruptHandler();

        return;

    }

        

    //割り込みハンドラを設定

    interrupted(interruptHandler);

        

    //イベントをキャッチしたら再びイベントをリスンして待機状態へ

    event(…

テストコードに上記の処理を追加すればテストは通ります。

  .
Time: 0.337
OK (1 test)

うまくいったように思えます。が、まだ2つの問題を抱えています。

1つ目は、待機状態(WAITING)でないのに割り込みハンドラを呼び出すという意味でThreadの仕様を変えてしまっている事です。
2つ目は、実はまだinterrupt()のタイミングによってはテストが失敗するという事です。

後者について詳しく見ていきます。

まず、現段階のテストでは、イベントをキャッチした後割り込みが実行されています。([イベント] →  [割り込み])。しかし、割込みを実行した後にイベントをキャッチするケース([割り込み] → [イベント])をまだテストしていません。

テストコードにテストを追加してみます。

//割り込み直後にイベント配信

test function inturruptHandlerExecution2() : void

{

    //200mSec後に [イベント配信] -> [割り込み]

    setTimeout(

        function():void

        {

            //Threadinterrupt()を呼び出し

            trace(“¥n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!¥n”);

            thread.interrupt();

            //Threadに向けてイベントを配信

            dispatcher.dispatch();

                    

        }, 200

    );

    //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

    setTimeout(

        function():void

        {

            dispatcher.dispatch();

        },

        250

    );

            

    //300mSec後にテスト

    setTimeout(

        async(

            function():void

            {

                //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                assertEquals(“interrupt event interrutHandler finalize “, Static.log);

                //割り込み後のステータスをテスト

                assertEquals(ThreadState.TERMINATED, thread.state);

                //割り込み後の割り込みフラグをチェック(待機状態での割り込みなのでfalseのはず)

                assertFalse(thread.isInterrupted);

            }

        ), 

        300

    );

                

    //テスト用Thread開始

    thread.start();

}

前は「イベント配信」「割り込み」という順番でしたが、今度のテストは「割り込み」「イベント配信」という順番です。前のテストとはこの呼び出しの順番だけが違います。

このテストを実行してみるとテストは失敗します。

.E.
Time: 0.658
There was 1 failure:
1) inturruptHandlerExecution2(org.libspark.thread::InterruptHandlerTest)
Error: expected:<interrupt event [interrutHandler] > but was:<interrupt event [event] >

これはwonderflでsoundkitchenさんが指摘していることと同じことが起こっています。

図にすると以下のような感じです。

another failure case

このケースでは割り込み後のイベントハンドラの動作で、割り込みフラグと割り込みハンドラが上書きされてしまいます。
こうなるともはやThreadは自分が割り込まれたかどうかを知る手段がありません。運用でカバーするのは無理そうです。
やはりこの問題を解決するにはThreadのソースコードに手を入れる必要があるかもしれません。

次回のエントリでは解決編として、Threadのソースにパッチを当てる方法を書いてみます。

つづく

カテゴリー: ActionScript3 パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です