Process.Start を AppDomains に置き換える
-
19-09-2019 - |
質問
背景
さまざまなサードパーティ DLL を使用して PDF ファイルの作業を実行する Windows サービスがあります。これらの操作はかなりの量のシステム リソースを使用する可能性があり、エラーが発生したときにメモリ リークが発生することがあります。DLL は、他のアンマネージ DLL のマネージ ラッパーです。
現在のソリューション
あるケースでは、DLL の 1 つへの呼び出しを専用のコンソール アプリでラップし、Process.Start() 経由でそのアプリを呼び出すことで、この問題をすでに軽減しています。操作が失敗し、メモリ リークや未解放のファイル ハンドルがあったとしても、それはあまり問題ではありません。プロセスが終了し、OS がハンドルを回復します。
これと同じロジックを、これらの DLL を使用するアプリ内の他の場所に適用したいと思います。ただし、自分のソリューションにさらにコンソール プロジェクトを追加したり、Process.Start() を呼び出してコンソール アプリの出力を解析する定型コードをさらに記述したりすることには、それほど興奮していません。
新しいソリューション
専用のコンソール アプリと Process.Start() に代わるエレガントな代替手段は、次のように AppDomain を使用することのようです。 http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx.
同様のコードをアプリケーションに実装しましたが、単体テストの結果は期待できませんでした。別の AppDomain 内のテスト ファイルへの FileStream を作成しますが、それを破棄しません。次に、メイン ドメインで別の FileStream を作成しようとしますが、ファイル ロックが解放されていないために失敗します。
興味深いことに、空の DomainUnload イベントをワーカー ドメインに追加すると単体テストに合格します。いずれにしても、「ワーカー」AppDomain を作成しても問題は解決しないのではないかと心配しています。
考えは?
コード
/// <summary>
/// Executes a method in a separate AppDomain. This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public T RunInAppDomain<T>( Func<T> func )
{
AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null,
new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } );
domain.DomainUnload += ( sender, e ) =>
{
// this empty event handler fixes the unit test, but I don't know why
};
try
{
domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke );
return (T)domain.GetData ( "result" );
}
finally
{
AppDomain.Unload ( domain );
}
}
public void RunInAppDomain( Action func )
{
RunInAppDomain ( () => { func (); return 0; } );
}
/// <summary>
/// Provides a serializable wrapper around a delegate.
/// </summary>
[Serializable]
private class AppDomainDelegateWrapper : MarshalByRefObject
{
private readonly AppDomain _domain;
private readonly Delegate _delegate;
public AppDomainDelegateWrapper( AppDomain domain, Delegate func )
{
_domain = domain;
_delegate = func;
}
public void Invoke()
{
_domain.SetData ( "result", _delegate.DynamicInvoke () );
}
}
単体テスト
[Test]
public void RunInAppDomainCleanupCheck()
{
const string path = @"../../Output/appdomain-hanging-file.txt";
using( var file = File.CreateText ( path ) )
{
file.WriteLine( "test" );
}
// verify that file handles that aren't closed in an AppDomain-wrapped call are cleaned up after the call returns
Portal.ProcessService.RunInAppDomain ( () =>
{
// open a test file, but don't release it. The handle should be released when the AppDomain is unloaded
new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None );
} );
// sleeping for a while doesn't make a difference
//Thread.Sleep ( 10000 );
// creating a new FileStream will fail if the DomainUnload event is not bound
using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) )
{
}
}
解決
アプリケーション ドメインとクロスドメイン インタラクションは非常に重要な問題なので、何かを行う前に、物事がどのように機能するかを本当に理解していることを確認する必要があります...うーん...「非標準」としましょう:-)
まず第一に、ストリーム作成メソッドは実際に「デフォルト」ドメインで実行されます (驚き、驚き!)。なぜ?単純:渡すメソッド AppDomain.DoCallBack
で定義されています AppDomainDelegateWrapper
オブジェクトがあり、そのオブジェクトはデフォルトのドメインに存在するため、そこでそのメソッドが実行されます。MSDN はこの小さな「機能」については述べていませんが、確認するのは簡単です。ブレークポイントを設定するだけです AppDomainDelegateWrapper.Invoke
.
したがって、基本的には「ラッパー」オブジェクトなしで対応する必要があります。DoCallBackの引数には静的メソッドを使用します。
しかし、静的メソッドがそれを取得して実行できるように、「func」引数を他のドメインに渡すにはどうすればよいでしょうか?
最も明白な方法は使用することです AppDomain.SetData
, または、自分でロールすることもできますが、どのように正確に実行するかに関係なく、別の問題が発生します。「func」が非静的メソッドの場合、それが定義されているオブジェクトを何らかの方法で他のアプリドメインに渡す必要があります。これは、値によって (フィールドごとにコピーされる場合)、または参照によって (リモート処理の利点をすべて備えたクロスドメイン オブジェクト参照を作成して) 渡すことができます。前者を行うには、クラスを でマークする必要があります。 [Serializable]
属性。後者を行うには、から継承する必要があります MarshalByRefObject
. 。クラスがそのどちらでもない場合、オブジェクトを他のドメインに渡そうとすると例外がスローされます。ただし、参照渡しではアイデア全体がほぼ台無しになることに注意してください。メソッドはオブジェクトが存在するのと同じドメイン、つまりデフォルトのドメインで呼び出され続けるためです。
上の段落を終えると、次の 2 つの選択肢が残ります。でマークされたクラスで定義されたメソッドを渡すか、 [Serializable]
属性を指定するか (オブジェクトがコピーされることに注意してください)、静的メソッドを渡します。あなたの目的のためには、前者が必要になると思います。
そして、あなたの注意を逸れた場合に備えて、2 番目の過負荷であることを指摘しておきたいと思います。 RunInAppDomain
(かかるもの Action
) マークされていないクラスで定義されたメソッドを渡します [Serializable]
. 。そこにクラスが表示されませんか?次のことを行う必要はありません。バインドされた変数を含む匿名デリゲートを使用すると、コンパイラが変数を作成します。そして、たまたま、コンパイラーがその自動生成されたクラスをわざわざマークしなかったのです。 [Serializable]
. 。残念ですが、これも人生です:-)
ここまでは言っておきましたが(言葉数が多くなりましたね?):-) そして、非静的および非-[Serializable]
メソッド、これが新しいメソッドです RunInAppDomain
メソッド:
/// <summary>
/// Executes a method in a separate AppDomain. This should serve as a simple replacement
/// of running code in a separate process via a console app.
/// </summary>
public static T RunInAppDomain<T>(Func<T> func)
{
AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null,
new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory });
try
{
domain.SetData("toInvoke", func);
domain.DoCallBack(() =>
{
var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func<T>;
AppDomain.CurrentDomain.SetData("result", f());
});
return (T)domain.GetData("result");
}
finally
{
AppDomain.Unload(domain);
}
}
[Serializable]
private class ActionDelegateWrapper
{
public Action Func;
public int Invoke()
{
Func();
return 0;
}
}
public static void RunInAppDomain(Action func)
{
RunInAppDomain<int>( new ActionDelegateWrapper { Func = func }.Invoke );
}
まだお付き合いいただけましたら、よろしくお願いします:-)
さて、そのメカニズムを修正するのに非常に多くの時間を費やした後、私はあなたに、とにかくそれは無意味だったと言いたいと思います。
問題は、AppDomains はあなたの目的には役に立たないということです。これらはマネージド オブジェクトのみを処理しますが、アンマネージド コードは必要なだけリークしたりクラッシュしたりする可能性があります。アンマネージ コードは、アプリドメインなどの存在さえ認識しません。プロセスについてのみ知っています。
したがって、最終的には、最善の選択肢が現在のソリューションのままになります。別のプロセスを生成して満足するだけです。そして、私も以前の回答に同意します。ケースごとに別のコンソール アプリを作成する必要はありません。静的メソッドの完全修飾名を渡すだけで、コンソール アプリにアセンブリを読み込み、型を読み込み、メソッドを呼び出します。実際、AppDomains で試したのとほぼ同じ方法で、かなりきれいにパッケージ化できます。「RunInAnotherProcess」のようなメソッドを作成すると、引数を調べ、そこから完全な型名とメソッド名を取得し(メソッドが静的であることを確認しながら)、残りの処理を行うコンソール アプリを生成します。
他のヒント
多数のコンソール アプリケーションを作成する必要はありません。完全修飾型名をパラメータとして受け取る単一のアプリケーションを作成できます。アプリケーションはその型をロードして実行します。
すべてのリソースを実際に処分するには、すべてを小さなプロセスに分割することが最善の方法です。アン アプリケーションドメイン リソースを完全に破棄することはできませんが、プロセスは破棄できます。
検討しましたか パイプを開ける メインアプリケーションとサブアプリケーションの間?こうすることで、標準出力を解析せずに、2 つのアプリケーション間でより構造化された情報を渡すことができます。