LoginSignup
9
7

More than 3 years have passed since last update.

C#のDI ContainerのIDisposableへの参照保持について

Last updated at Posted at 2019-03-24

C#のDI Containerはいろいろ (https://github.com/danielpalme/IocPerformance) 存在します。一部のコンテナでは自動DisposeのためにIDisposableインスタンスへの参照が保持(Tracking)され、注意しないとMemoryOverflowExceptionが発生することがあります。Trackingするコンテナとそうでないコンテナを調査してみました。

結果

各種DI Containerをできるだけデフォルト状態で調査したところ、以下のようになりました。

DI Container Normal Disposable
Autofac No Track
LightInject No No
abioc No No
DryIoc(NoTrack) No No
DryIoc(Track) No Track
Grace No Track
MicroResolver No No
Unity.Container No No

AutofacとGraceがIDisposableをTrackingするようです。

DryIocは2つありますが、DryIocだとどちらの挙動がよいか設定で選択できます。なにも選択しない状態だとIDisposableを実装する型を登録するとエラーが発生し、Trackingするかどうかを指示するように警告されるので、結構親切ですね。

なお、調査したときのバージョンは以下のとおりです。

  • Autofac, Version=4.9.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da
  • LightInject, Version=5.4.0.0, Culture=neutral, PublicKeyToken=null
  • abioc, Version=0.7.0.0, Culture=neutral, PublicKeyToken=null
  • DryIoc, Version=4.0.0.0
  • Grace, Version=6.4.2.0, Culture=neutral, PublicKeyToken=b7d24c6367970497
  • MicroResolver, Version=2.3.5.0, Culture=neutral, PublicKeyToken=null
  • Unity.Container, Version=5.10.2.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0

調査の発端

ここからは余談ですが、

C#アプリでAutofacを使用していたのですが、バッチ処理とかで大量のクラス生成を繰り返す処理を実行すると、アプリ内ではどこからも参照されていないのに、インスタンスが解放されず、OutOfMemoryExceptionが発生する問題に遭遇しました。

検索すると以下の記事がヒットします。

Autofac holds references to all the disposable components it creates

「Autofacは生成したすべてのIDsposableインスタンスへの参照を保持する」ので、

以下のようにLifetimeScopeで囲ってあげないとMemoryLeakしてMemoryOverflowExceptionになる。lifetimeScope.Resolve<IMyResource>();container.Resolve<IMyResource>();と書いてしまうとMemoryLeakになるということですね。

// var container = …
while (true)
{
    using (var lifetimeScope = container.BeginLifetimeScope())
    {
        var r = lifetimeScope.Resolve<IMyResource>();
        // r, all of its dependencies and any other components
        // created indirectly will be released here
    }
}

Don’t resolve from the root container. Always resolve from and then release a lifetime scope.

「常にlifetime scopeからインスタンスを生成し、タスク完了時にscopeをdisposeしなければならない」

Autofacの思想としては、大量データの生成、破棄をともなう繰り返し処理はコンテナに依存するようにし、厳密にLifetime管理せよ、ということなのでしょう。Autofacは参照しているIDisposableインスタンスのDispose()を自動で呼び出してくれるようで、それも便利なケースもありそうですね。

Autofacより後発のLightInjectではこのようなメモリリークは発生しません。前掲の記事によると、TrackingなDI ContainerとそうでないDI Containerがあるようです。そこで、C#のDI ContainerをTrackingなDI Containerとそれ以外で分類してみました。

調査方法

InnerRunメソッドのスコープ内でResolveしたオブジェクトのFinalizeをcallbackで通知を受け、GC.Collect()後にFinalizeされたかどうかを返します。

public bool Run(Type t)
{
    var container = CreateContainer(t);
    var finalized = false;
    InnerRun(container, t, () => {finalized = true;});

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    return finalized;
}

private void InnerRun(object container, Type t, Action callback)
{
    // Resolve in this scope.
    var obj = Resolve(container, t);
    obj.FinalizeCallback = callback;

    // Scope out, then no reference to obj.
}

Finalizeを監視する対象のBaseクラスは以下のようにしました。これを通常のオブジェクト(Normal)とIDisposableを実装したオブジェクトそれぞれに継承させるようにしています。

public abstract class FinalizeCallbackable
{
    public Action FinalizeCallback {get; set;} = null;

    ~FinalizeCallbackable()
    {
        Debug.WriteLine($"Finalize({this.GetType()})");
        FinalizeCallback?.Invoke();
    }
}

ソースコードはこちらです。.NET Core 2.2で動作確認しました。
https://github.com/youmts/dicontainer-tracking-checker/

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7