Quantcast
Channel: C# – grabacr.nét
Viewing all articles
Browse latest Browse all 30

バックグラウンド スレッドで UI 要素を作るとメモリリークする (WPF)

$
0
0

題記の通りなのですが、一時ハマったので共有します。 正確には、「バックグラウンド スレッドで DispatcherObject を作るとメモリリークする」ですね。

例えば、バックグラウンドで画像をダウンロードして加工したり。 もしくは Grid や TextBlock といった UI 要素を使って、サムネイルやら何やらの画像を生成したいときとか。

ImageSource は Freezable なので、バックグラウンドで画像を作って Freeze() してしまえば、UI スレッドに渡してもだいじょうぶ。 なるべく UI スレッドの負担を減らしたいのです。


ということで、以下のようなコードを書いてみます。 サンプルなので Console アプリですが、PresentationCore, PresentationFramework, WindowsBase, System.Xaml あたりを参照すればよろし。

static void Work()
{
    var border = new Border { Background = Brushes.Red };
    var text = new TextBlock { Text = "hoge" };

    border.Child = text;
    border.Arrange(new Rect(0, 0, 200, 200));

    var bitmap = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Default);
    bitmap.Render(border);
    bitmap.Freeze();

    // 生成したビットマップ使って何かする
}

で、このビットマップを作るコードを、バックグラウンドスレッドで動かします。 試しに 10 回ほど。

static void Main(string[] args)
{
    for (var i = 0; i < 10; i++)
    {
        var thread = new Thread(Work)
        {
            Name = "Bitmap worker: " + i,
            IsBackground = true,
        };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        Console.WriteLine("Total Memory = {0} KB", GC.GetTotalMemory(true) / 1024);
    }

    Console.ReadLine();
}

ちなみに、バックグラウンドでやろうとすると「多数の UI コンポーネントが必要としているため、STA である必要があります。」といった例外がスローされるようなもの (XpsDocument とか) でも、Thread.SetApartmentState メソッドでスレッドを STA にしてやれば、ちゃんと動きます。

これを実行すると…

memory leak

メモリ使用量がどんどん増えていきます。 これではダメですね。

そこで、最初のコードに以下の 2 行を追加します。

static void Work()
{
    var border = new Border { Background = Brushes.Red };
    var text = new TextBlock { Text = "hoge" };

    border.Child = text;
    border.Arrange(new Rect(0, 0, 200, 200));

    var bitmap = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Default);
    bitmap.Render(border);
    bitmap.Freeze();

    // 生成したビットマップ使って何かする

    Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
    Dispatcher.Run();
}

ご覧の通り、Dispatcher をシャットダウンするコードです。 これを実行すると…

いい感じです。


さて、この原因についてですが。

DispatcherObject を生成した時点で、スレッドごとに新たな Dispatcher が生成されるため、…と考えられます。 (Dispatcher のソースコードを軽く読んだところ、スレッドごとに生成された Dispatcher は WeakReference で保持されてるようですが、果たして…?)

ご参考まで。 詳しい事情が判ったらまた展開します。


2014/05/20 追記:

@karno 氏が詳しい調査をしてくださいました。

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

Krile STARRYEYES に入った修正も参考にしつつ。メイン スレッド外で DispatcherObject を生成する場合は、かなり気を遣いましょう。

WPF に関する型ってだいたい DispatcherObject の派生型なので、気付かず使ってるパターンもありそうです。Dispatcher のシャットダウンまで面倒見てくれるユーティリティ等を用意したほうがよさげ、かな。


Viewing all articles
Browse latest Browse all 30

Trending Articles