バックグラウンドスレッドでUI要素を作るともっと問題は深刻かもしれない。(WPF)

お久しぶりです。

ぐらばくさん(@Grabacr07)のブログ記事、バックグラウンド スレッドで UI 要素を作るとメモリリークする (WPF) | grabacr.nét が個人的に話題です。

この問題について、もうちょっと詳しく調べてみました。

そもそもの問題

DispatcherObject という、WPFの基盤になっているクラスがあります。みなさんがご利用の TextBlock、BitmapImage や Freezableやなんやかんやの基本クラスは全部 DependencyObjectで、その基本クラスがDispatcherObjectです。

DispatcherObjectは、作成時にそのスレッドに Dispatcher が存在するか確認し、なければ作ります。

そして黙って作成されたDispatcherはなんとメモリリークするようなのです。

何が起きているのか

基本的に CLR 上のオブジェクトは参照されなくなると勝手にGCされて消えてなくなります。参照にはイベントのサブスクリプションも含まれるので気をつけるのはWPFの定石です。

しかしDispatcherはきちんとGCされているようです。というのも、Dispatcherは作成時に自身が持つリストに保持されるのですが、それがきちんと増えたり減ったりしているからです(参照: Dispatcher.cs – Microsoft Reference Source)

では何が問題なのか。

リソースリーク

私はここで、もう一つ気になる記事を見つけました。

BitmapDecoder を別スレッドで作成するとリソースリークする – c++ 使いの c# メモ

ここでは、「RegisterClassEx」によって登録されたウィンドウクラスが開放されないみたいです、という記述があります。

先ほどのDispatcher.csのコンストラクタを読むと、MessageOnlyHwndWrapperを作成している箇所があります。このMessageOnlyHwndWrapperはHwndWrapperから派生していますので、これをあたると…

ありました。RegisterClassExされています。

しかし.NETのお作法として、ネイティブリソースを扱うときはIDisposableを実装して、Disposeメソッドかファイナライザからそのリソースを消し去るようにするというものがあります。

当然HwndWrapperでもファイナライザからDispose(bool)を呼ぶようになっていますが、ちょっとトリッキーなことになっています。今回の場合は、UnregisterClassにウィンドウクラスを消し去るメソッドがありますが、そこに至るまでにはまずウィンドウを消す必要があるようです。ということで、Disposeメソッドの実装ですが、

  1. ウィンドウを殺す
  2. ウィンドウを殺したメッセージがWndProcに流れてくる
  3. ウィンドウクラスを殺す

という流れになっています。

上記の内容には誤りがありましたので、お詫びして以下の通り訂正いたします:

今回もファイナライザからDisposeメソッドが呼ばれますが、ファイナライザが呼ばれるのはファイナライザスレッドからなので、245行目でelse側に分岐します。

245行目以降でDestroyWindowが呼ばれ、ウィンドウハンドルとウィンドウクラスが破棄されてめでたくリソースが開放されるわけです。

ですが、これらのメソッドが呼ばれるのは今回はファイナライザ スレッド上です。ですので、スレッド間の同期を取るためディスパッチャを経由するわけです
(Dispatcher.BeginInvoke)が、ファイナライザスレッドではDispatcher.Runされていないため、BeginInvokeしたまま帰ってこないのでしょう…おいたわしや。

ウィンドウクラスは何故アプリケーションを超えてリークするのか

さて、リークの概要は掴めましたが、もう一つ不思議な点があります。

特にWindows NT以降、一つのアプリケーションがリソースを食い漁ったところで、他のアプリケーションにその影響が波及することはほぼありません。ですが、先述した記事の中では

Windows XPの場合、RegisterClassEx で登録されたウィンドウクラスは、プロセスを終了しても残り、その他のほとんどのアプリケーションがまともに動作しなくなります。

とあります。本当でしょうか。

ここで今回コールしているRegisterClassEx関数を見てみましょう。

アプリケーションが登録したウィンドウクラスは、アプリケーション終了時にすべて自動的に削除されます。

(中略)

Windows NT/2000:.dll により登録されたウィンドウクラスは、その .dll がアンロードされても一切削除されません。

今回 Dispatcher.cs こと Dispatcher クラスは、 WindowsBase.dll にあります。

Windows NT/2000:.dll により登録されたウィンドウクラスは、その .dll がアンロードされても一切削除されません。

Windows NT/2000:.dll により登録されたウィンドウクラスは、その .dll がアンロードされても一切削除されません。

Windows NT/2000:.dll により登録されたウィンドウクラスは、その .dll がアンロードされても一切削除されません。

 

(´つヮ⊂)ウオオオオwww

対策

バックグラウンドスレッドでDispatcherを作るのをやめるか、作ったとしても殺せばいいわけです。

つまり、以下の様な対策が取れます。

  • バックグラウンドスレッドでDispatcherObjectを作らない
  • バックグラウンドスレッドでDispatcherObjectを作った時はBeginInvokeShutdownを呼んで Dispatcher.Run する(ぐらばく氏の記事参照)

ちなみに、一度シャットダウンしたDispatcherは二度と戻ってはきません。そのため、Threadでwhileループしている途中でDispatcherをシャットダウンすると死にますので、くれぐれも計画的に。

まとめ

 

  • DispatcherObjectをバックグラウンドで作る時には気をつけること!
  • 作ったらちゃんと後始末すること!
  • そうでないとユーザにPCの再起動を強いるアプリケーションが完成するぞ!

以上です。

DispatcherObjectはいろんなところに隠れているので(Freezableとか特に)、対策はけっこう手間がかかるかもしれませんが、頑張って参りましょう。

コメントを残す

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