24

Visual Studio 2010 を使用して .NET 4.0 クライアント プロファイルをターゲットにしています。特定のプロセスの開始/終了を検出する C# クラスがあります。このために、クラスは以下のように初期化される ManagementEventWatcher を使用します。queryscopeおよびwatcherクラス フィールドです。

query = new WqlEventQuery();
query.EventClassName = "__InstanceOperationEvent";
query.WithinInterval = new TimeSpan(0, 0, 1);
query.Condition = "TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'notepad.exe'";

scope = new ManagementScope(@"\\.\root\CIMV2");

watcher = new ManagementEventWatcher(scope, query);
watcher.EventArrived += WatcherEventArrived;
watcher.Start();

イベント EventArrived のハンドラーは次のようになります。

private void WatcherEventArrived(object sender, EventArrivedEventArgs e)
{
    string eventName;

    var mbo = e.NewEvent;
    eventName = mbo.ClassPath.ClassName;
    mbo.Dispose();

    if (eventName.CompareTo("__InstanceCreationEvent") == 0)
    {
        Console.WriteLine("Started");
    }
    else if (eventName.CompareTo("__InstanceDeletionEvent") == 0)
    {
        Console.WriteLine("Terminated");
    }
}

このコードは、CodeProject の記事に基づいています。メモリ リークが発生したため、呼び出しを追加しましたmbo.Dispose(): EventArrived が発生するたびに約 32 KB、1 秒に 1 回。リークは、WinXP と Win7 (64 ビット) の両方で明らかです。

ここまでは順調ですね。良心的になろうとしてtry-finally、次のような句を追加しました。

var mbo = e.NewEvent;
try
{
    eventName = mbo.ClassPath.ClassName;
}
finally
{
    mbo.Dispose();
}

問題ありません。さらに良いことに、C#using句はよりコンパクトですが同等です。

using (var mbo = e.NewEvent)
{
    eventName = mbo.ClassPath.ClassName;
}

メモリ リークが再発しました。どうしたの?

まあ、私は知りません。しかし、ILDASM で 2 つのバージョンを逆アセンブルしてみましたが、ほとんど同じではありません。

イリノイからtry-finally:

.try
{
  IL_0030:  nop
  IL_0031:  ldloc.s    mbo
  IL_0033:  callvirt   instance class [System.Management]System.Management.ManagementPath [System.Management]System.Management.ManagementBaseObject::get_ClassPath()
  IL_0038:  callvirt   instance string [System.Management]System.Management.ManagementPath::get_ClassName()
  IL_003d:  stloc.3
  IL_003e:  nop
  IL_003f:  leave.s    IL_004f
}  // end .try
finally
{
  IL_0041:  nop
  IL_0042:  ldloc.s    mbo
  IL_0044:  callvirt   instance void [System.Management]System.Management.ManagementBaseObject::Dispose()
  IL_0049:  nop
  IL_004a:  ldnull
  IL_004b:  stloc.s    mbo
  IL_004d:  nop
  IL_004e:  endfinally
}  // end handler
IL_004f:  nop

イリノイからusing:

.try
{
  IL_002d:  ldloc.2
  IL_002e:  callvirt   instance class [System.Management]System.Management.ManagementPath [System.Management]System.Management.ManagementBaseObject::get_ClassPath()
  IL_0033:  callvirt   instance string [System.Management]System.Management.ManagementPath::get_ClassName()
  IL_0038:  stloc.1
  IL_0039:  leave.s    IL_0045
}  // end .try
finally
{
  IL_003b:  ldloc.2
  IL_003c:  brfalse.s  IL_0044
  IL_003e:  ldloc.2
  IL_003f:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0044:  endfinally
}  // end handler
IL_0045:  ldloc.1

どうやら問題は次の行です。

IL_003c:  brfalse.s  IL_0044

これは と同等であるためif (mbo != null)mbo.Dispose()呼び出されることはありません。しかし、mbo がアクセスできた場合、mbo が null になる可能性はあります.ClassPath.ClassNameか?

これについて何か考えはありますか?

また、この動作がここで未解決の議論を説明するのに役立つかどうか疑問に思っています:イベント ログを照会するときの WMI でのメモリ リーク

4

3 に答える 3

35

一見すると、にバグがあるように見えますManagementBaseObject

からのDispose()メソッドは次のManagementBaseObjectとおりです。

    public new void Dispose() 
    {
        if (_wbemObject != null) 
        {
            _wbemObject.Dispose();
            _wbemObject = null;
        } 
        base.Dispose();
        GC.SuppressFinalize(this); 
    } 

として宣言されていることに注意してくださいnew。また、usingステートメントが を呼び出すときはDispose、明示的なインターフェイスの実装を使用して呼び出していることにも注意してください。したがって、親Component.Dispose()メソッドが呼び出され、呼び出さ_wbemObject.Dispose()れることはありません。 ManagementBaseObject.Dispose()hereのように宣言しないでnewください。信じられない?メソッドComponent.csのすぐ上にあるからのコメントを次に示します。Dispose(bool)

    ///    <para>
    ///    For base classes, you should never override the Finalier (~Class in C#) 
    ///    or the Dispose method that takes no arguments, rather you should 
    ///    always override the Dispose method that takes a bool.
    ///    </para> 
    ///    <code>
    ///    protected override void Dispose(bool disposing) {
    ///        if (disposing) {
    ///            if (myobject != null) { 
    ///                myobject.Dispose();
    ///                myobject = null; 
    ///            } 
    ///        }
    ///        if (myhandle != IntPtr.Zero) { 
    ///            NativeMethods.Release(myhandle);
    ///            myhandle = IntPtr.Zero;
    ///        }
    ///        base.Dispose(disposing); 
    ///    }

ここではusingステートメントが明示的なIDisposable.Disposeメソッドを呼び出すため、newDispose が呼び出されることはありません。

編集

通常、このようなことをバグとは思いませんが、newforを使用することDisposeは通常悪い習慣であり (特にManagementBaseObjectは封印されていないため)、の使用について説明するコメントがないため、newこれはバグだと思います。

この問題に関する Microsoft Connect のエントリが見つからなかったため、作成しました。再現できる場合、またはこれが影響した場合は、お気軽に賛成票を投じてください。

于 2012-08-10T06:11:40.413 に答える
0

また、この問題により、MS Unit Test Framework が失敗し、すべてのテストの実行の最後に永久にハングアップします (Visual Studio 2015、update 3 で)。残念ながら、これを書いている現在もバグは解決していません。私の場合、次のコードがリークしています。

using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
    ....
}

そして、テスト フレームワークが不平を言っているのは、スレッドがシャットダウンされていないことです。

System.AppDomainUnloadedException: アンロードされた AppDomain にアクセスしようとしました。これは、テストがスレッドを開始したが停止しなかった場合に発生する可能性があります。テストによって開始されたすべてのスレッドが完了前に停止されていることを確認してください。

そして、別のスレッドでコードを実行することで問題を回避することができました (したがって、スターター スレッドが終了した後、スターター スレッドで生成された他のすべてのスレッドが閉じられ、リソースが適切に解放されることを願っています)。

Thread searcherThread = new Thread(new ThreadStart(() =>
{
    using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
    {
        ....
    }
}));
searcherThread.Start();
searcherThread.Join();

これが問題の解決策であると主張しているわけではありません (実際のところ、この呼び出しのためだけにスレッドを生成するのは恐ろしい考えです) が、少なくとも、ハングするたびに Visual Studio を再起動する必要なく、テストを再度実行できます。 .

于 2016-07-26T22:34:17.353 に答える