c# BitmapDecoder を別スレッドで作成するとリソースリークする

C# に限りませんが、.NET の BitmapDecoderCreate を別スレッドで呼び出すと、リソースリークします。

現象

RegisterClassEx により登録されたウィンドウクラスが開放されないみたいです。32 ビットバージョンの Windows では、16000、64 ビットバージョンの Windows では、63000 回程度の繰り返しで、BitmapDecoder が作成できなくなります。

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

解決方法

Thread の代わりに、BackgroundWorker を使うと、リソースは自動的に開放されるみたいです。Dispatcher.CurrentDispatcherInvokeShutdown() を呼ぶ方法もありますが、あまり、おすすめしません。詳しくはコードを見てください。

1回の実行を抽象化したクラス

Do()、テストに成功した場合のみ、true を返すことにします。

using System;

namespace SSS.Test
{
  public abstract class Test
  {
    public abstract bool Do();
  }
}

今回のテスト

path で読み込む画像ファイルのパスを指定します。適宜変更してください。

using System;
using System.Windows.Media.Imaging;

namespace UnitTest
{
  class BitmapDecoderTest : SSS.Test.Test
  {
    // const string path = @"C:\Windows\Web\Wallpaper\img1.jpg";
    const string path = @"C:\WINDOWS\Web\Wallpaper\夕陽の砂丘.jpg";
    const BitmapCreateOptions createOption = BitmapCreateOptions.None;
    const BitmapCacheOption cacheOption = BitmapCacheOption.Default;

    public override bool Do()
    {
      BitmapDecoder decoder = BitmapDecoder.Create(
        new Uri(path), createOption, cacheOption
      );
      return true;
    }
  }
}

別スレッドでテストを実行するコード

Test() を呼び出すと、「別スレッドで、引数 test の Do() を実行、終了を待つ」を、無限に繰り返します。invokeShutdown を true にするとリソースリークしません。つまり、CurrentDispatcher の InvokeShutdown() を呼ぶことでリソースを開放できます。

using System;
using System.Threading;
using System.Windows.Threading;

namespace SSS.Test
{
  public class ThreadTester
  {
    public bool EndTest = false;
    protected Test test;
    protected bool invokeShutdown = false;

    public ThreadTester(Test test, bool invokeShutdown)
    {
      this.test = test;
      this.invokeShutdown = invokeShutdown;
    }

    public void Do()
    {
      try
      {
        EndTest = !test.Do();
      }
      catch (Exception err)
      {
        EndTest = true;
        Console.WriteLine(err);
      }  
      finally
      {
        if (invokeShutdown)
        {
          Dispatcher dsp = Dispatcher.FromThread(Thread.CurrentThread);
          if (dsp != null)
            dsp.InvokeShutdown();
        }
      }
    }

    public static void Test(Test test, bool invokeShutdown)
    {
      var tester = new ThreadTester(test, invokeShutdown);

      for (int i = 0;; ++ i)
      {
        Thread thread = new Thread(new ThreadStart(tester.Do));
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();

        if (i % 100 == 99)
          Console.WriteLine(i + 1);
        if (tester.EndTest)
          break;
      }
    }
  }
}

メインプログラム

ThreadTester.Test() の引数で、invokeShutdown を false に設定しているのでリークします。(2) のように、true にすると一応、リークしません。(3) のように、BackgroundWorkerTester を使うと、さらに良い結果になります。

using System;
using SSS.Test;

namespace UnitTest
{
  class Program
  {
    [STAThread]
    static void Main(string[] args)
    {
      ThreadTester.Test(new BitmapDecoderTest(), false); // (1)
      // ThreadTester.Test(new BitmapDecoderTest(), true); // (2)
      // BackgroundWorkerTester.Test(new BitmapDecoderTest()); // (3)
    }
  }
}

Thread の代わりに、BackgroundWorker を使うもの

上の (3) に対応する、BackgroundWorkerTester です。こちらでは、InvokeShutdown を呼ばなくてもリソースリークしません。

Thread を使って InvokeShutdown した場合には、少しずつ、メモリーの使用量が増え、33 MByte くらいで安定しますが、BackgroundWorker を使用した場合、実行してすぐに、22 MByte くらいで安定します。(Windows XP x64 の場合)

想像するに、Thread を使った場合には、ガベージコレクターまかせで開放されることになる、ネイティブリソースがあるみたいです。うーん、何なんだ?

using System;
using System.ComponentModel;

namespace SSS.Test
{
  public class BackgroundWorkerTester
  {
    public bool EndTest = false;
    protected Test test;

    public BackgroundWorkerTester(Test test)
    {
      this.test = test;
    }

    public void Do(object sender, DoWorkEventArgs e)
    {
      try
      {
        EndTest = !test.Do();
      }
      catch (Exception err)
      {
        EndTest = true;
        Console.WriteLine(err);
      }
    }

    public static void Test(Test test)
    {
      var tester = new BackgroundWorkerTester(test);

      for (int i = 0;; ++ i)
      {
        var worker = new BackgroundWorker();
        worker.DoWork += tester.Do;
        worker.RunWorkerAsync();

        while (worker.IsBusy)
          System.Threading.Thread.Sleep(1);

        if (i % 100 == 99)
          Console.WriteLine(i + 1);
        if (tester.EndTest)
          break;
      }
    }
  }
}

結論

BackgroundWorker では、Thread に加えて、謎のリソースの開放処理が入っているみたいなので、より安全です。Thread で行っていた部分を BackgroundWorker で置き換えるのは、機能が増えるだけなので簡単です。

Tips の応用プログラム

この Tips は、ミルノ PC フォトフレーム のメモリーリークを解消する際に必要となった知識です。画像の読み込みは、別スレッドで行うのは自然なのに、MSDN には、何の注意もありません。不思議ですね・・・。

となりのページ

このサイトについて

このサイトのページへのリンクは自由に行っていただいてかまいません。
このサイトで公開している全ての画像、プログラム、文書の無断転載を禁止します。

連絡先

ここをクリック すると表示されるページから作者へメールで連絡できます。

共有