2006年04月02日

マルチメディアタイマ・改訂版

さて、前回アップしたマルチメディアタイマですが、速攻で問題点を見つけました。このままでは実行中に参照が無くなったとき、タイマが実行中でも止まってしまいます。

それはいけない。手本である System.Timers.Timer はタイマが有効な間は参照が無くなってもタイマを止めるまでは動き続けるようにデザインされています。

今回初めて知ったんですが、標準ライブラリの三つのタイマクラスのうち System.Threading.Timer クラスだけは実行中でも GC に回収されます(当然タイマは強制中断)。

とりあえずタイマが実行中に止まる確認。

public static void Main() {
    WeakReference wr = StartTimer();
    Thread.Sleep(1000);
    Console.WriteLine(wr.IsAlive);
    GC.Collect();
    Console.WriteLine(wr.IsAlive);
    Thread.Sleep(1000);
    Console.WriteLine(wr.IsAlive);
}
public static WeakReference StartTimer() {
    MultimeidaTimer timer = new MultimediaTimer();
    timer.Elapsed += delegate {
        Console.Write(".");
    };
    timer.Interval = 100;
    timer.Start();
    return new WeakReference(timer);
}

ってあれ、GC.Collect 呼んでも回収されていません。どうやら何か前提が間違っていたか、参照が無くなろうが全く止まる気配もありません。一体どうしたことでしょうか。

StartTimer メソッドで Start() の呼び出しをしなくても回収されません。実は MultimediaTimer そのものが現状、回収され得なくなっています。

原因はインスタンス作成時に設定している callback フィールドにありました。コンストラクタの中では、 MultimediaTimer のインスタンスメソッドである OnTicked メソッドのデリゲートインスタンスを作成し、それを GCHandle.Alloc で固定化し、callback フィールドにそのハンドルを格納しています。

どうでもいいことですが、静的メンバの対義語にインスタンスメンバを持ってくるのがすごく気持ち悪いです。対とするのならクラスメンバ<>インスタンスメンバなんでしょうけど、.NET ではクラスメンバではなく静的メンバの方が一般的に感じますし。ドキュメントでも静的メンバ<>インスタンスメンバという対になってるんですよね。静的の反対は普通動的ですが、この場面では動的は意味が違いますしねー。非静的メンバというのも据わりが悪いです。いい日本語はないものか。

このとき、このデリゲートインスタンスが GC の対象外になりますが、デリゲートインスタンスが GC から保護されていると、そのデリゲートが保持するインスタンスメソッドを持っているインスタンスそのものが GC の対象外となるようなのですね。確かにインスタンスメソッドを保護中に当のインスタンスそのものが GC に回収されてしまっては話になりません。ん〜、理屈的に分からなくもない挙動ですが変な感じです。

難儀なことにというべきか幸いなことにと言うべきか、アプリケーション終了時にはファイナライザが呼び出されます。一体どういう仕組みなんでしょ。

さて、これでどんな問題が発生するか。まず、Dispose した場合は問題ありません。問題はファイナライザに解放を任せた場合です。このときは GC の対象にならない以上、参照が無くなっても居座り続けます。非実行時はアンマネージドリソースを使っていないし大したコストはかかりませんが、GCHandle が長く居座り続けるのはあまり都合のいい話ではありません、また参照を残していないって事はなんかのイベントで使い捨てに使うことも多いでしょうから、次第にメモリを食いつぶしていくことになります。というか基底クラスの Component が Dispose 持ってるって事はこれアンマネージドリソースも持ってる可能性が。

ということで、実行中タイマの保護はこの GCHandle.Alloc で実現できるので、あとは適切な保護期間の設定が問題と言うことになります。つまり実行中以外では GCHandle で GC からデリゲートを保護するのを禁止しなければならないと言うことですね。当初とはまるで逆の目的になりましたが。

以前のコードではコンストラクタで GCHandle を確保していました。これはまずいと言うことになります。もともと GCHandle を使っているのは GC によってメソッドのポインタを移動させられアンマネージドのコンテキストが実行すべきメソッドを見失う、という事態を避けるためでした。ですから、アンマネージド部分が必要な部分だけ、つまり Start から Stop までだけ GCHandle を確保しておくと言うことになります。コンストラクタで Alloc するなど言語道断。

そうなるとほかにも影響が出てきます。なにせ GCHandle が確保されているかどうかでオブジェクトが Dispose されたかどうかを判断していたので。代わりにどうしましょうか。bool の disposed フィールドを新たに用意しても良いですが、あるものを使い回す方向で考えるのなら、SynchronizingObject 用の object[] なんかが具合良さそうです。あるいは interval フィールドを、破棄済みの場合は負数にすると言う手もあります、がこれはさすがに分かりづらいでしょう。まあ、オブジェクトに複数の意味を持たせないと言う原則に従ってここは素直に新しくフィールド disposed を追加しましょう。

まずコンストラクタ(厳密にはそこで呼んでる InitializeComponent メソッド)の GCHandle.Alloc を削除します。

次に影響が出るのは Start メソッド。ObjectDisposedException の条件が this.callback.IsAllocated でしたが、変更後はこれでは判断できなくなるので、代わりに this.disposed です。その後、TickedCallback デリゲートインスタンスを作成し、GCHandle.Alloc で GC から保護します。これで GCHandle.Free しない限りはインスタンスが保護されるようになります。それから、SetTimeEvent が失敗した場合、タイマは結局動いていないわけですから、この後インスタンスを保護していてはいけません。即刻 GCHandle を解放します。

次は Stop メソッド。今後のためデザインモードの判定だけをこのメソッドに残し、実際の終了処理は private な新しいメソッド StopInternal にやらせることにします。Dispose での処理とファイナライザでの終了処理を共通化するためです(ファイナライザが動くときはタイマは動いてないはずなので、理論的には必要ないのですが念のために)。自前部分だけならファイナライザで触って良いかどうか容易に判断できますからね。さて StopInternal メソッドでは、タイマを終了させた後、GCHandle を解放します。OnTicked メソッドでも、AutoReset が false の時は GCHandle の解放を行う必要がありますね。ちなみにここで単純に Stop を呼ばないのは KillTimeEvent を必要もなく呼び出すのを避けるためです。

最後に Dispose メソッドですが、まず Stop の代わりに StopInternal を呼び出してファイナライザでの動作を保証します。StopInternal では前述の通り GCHandle を解放していますのでここで改めて解放を記述する必要はありません。あとはとくに変更無し。

以上の変更を加えて、取りあえず完成です。まだバグが潜んでるような気はしますがそのときはそのときです。

posted by Hongliang at 21:35| Comment(2) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
下着 ランジェリー <a href="http://www.cleastjp.biz/" title="ジミーチュー 靴">ジミーチュー 靴</a>
Posted by ジミーチュー 靴 at 2013年07月17日 18:23
通販 ワンピース ルブタン 定番 http://www.cleasejp.biz/ [url=http://www.cleasejp.biz/]ルブタン 定番[/url]
Posted by ルブタン 定番 at 2013年07月23日 21:28
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。