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/