web-dev-qa-db-ja.com

Process.StartをAppDomainsで置き換えます

背景

さまざまなサードパーティのDLLを使用してPDFファイルで作業を実行するWindowsサービスがあります。これらの操作はかなりのシステムリソースを使用する可能性があり、エラーが発生するとメモリリークが発生することがあります。 。DLLは、他のアンマネージDLLのマネージラッパーです。

現在の解決策

専用のコンソールアプリでDLLの1つへの呼び出しをラップし、Process.Start()を介してそのアプリを呼び出すことで、この問題をすでに軽減している場合があります。操作が失敗し、メモリリークや解放されていないファイルハンドルがある場合、それは実際には問題ではありません。プロセスが終了し、OSがハンドルを回復します。

これらのDLLを使用するアプリ内の他の場所にも同じロジックを適用したいと思います。ただし、ソリューションにコンソールプロジェクトを追加したり、Process.Start()を呼び出してコンソールアプリの出力を解析したりするボイラープレートコードをさらに作成することに、それほど興奮していません。

新しいソリューション

専用のコンソールアプリとProcess.Start()のエレガントな代替手段は、次のようなAppDomainsの使用のようです: 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 ) )
    {
    }
}
49
MikeWyatt

アプリケーションドメインとクロスドメインの相互作用は非常に薄い問題なので、何かをする前に、彼が物事がどのように機能するかを本当に理解していることを確認する必要があります...うーん...「非標準」としましょう:-)

まず、ストリーム作成メソッドは実際には「デフォルト」ドメインで実行されます(サプライズ-サプライズ!)。どうして?単純:AppDomain.DoCallBackに渡すメソッドはAppDomainDelegateWrapperオブジェクトで定義され、そのオブジェクトはデフォルトドメインに存在するため、そのメソッドが実行されます。 MSDNはこの小さな「機能」については述べていませんが、確認するのは簡単です。AppDomainDelegateWrapper.Invokeにブレークポイントを設定するだけです。

したがって、基本的には、「ラッパー」オブジェクトなしで実行する必要があります。 DoCallBackの引数に静的メソッドを使用します。

しかし、静的メソッドがそれを取得して実行できるように、「func」引数を他のドメインにどのように渡すのでしょうか。

最も明白な方法はAppDomain.SetDataを使用することです。または、独自にロールすることもできますが、それをどの程度正確に行うかに関係なく、別の問題があります。「func」が非静的メソッドである場合、そのオブジェクトはで定義されているものは、何らかの方法で他のappdomainに渡される必要があります。値(コピーされるのに対し、フィールドごと)または参照(Remotingのすべての美しさを備えたクロスドメインオブジェクト参照を作成する)のいずれかで渡すことができます。前者を行うには、クラスに[Serializable]属性を付ける必要があります。後者を行うには、MarshalByRefObjectから継承する必要があります。クラスがどちらでもない場合、オブジェクトを他のドメインに渡そうとすると例外がスローされます。ただし、参照渡しを行うと、アイデア全体がほとんど失われることに注意してください。メソッドは、オブジェクトが存在するのと同じドメイン、つまりデフォルトのドメインで引き続き呼び出されるためです。

上記の段落を締めくくると、2つのオプションが残ります。[Serializable]属性でマークされたクラスで定義されたメソッドを渡すか(オブジェクトがコピーされることに注意してください)、静的メソッドを渡します。あなたの目的のために、前者が必要になると思います。

そして、それがあなたの注意を逃れた場合に備えて、RunInAppDomainの2番目のオーバーロード(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などがあることすら知りません。プロセスについてのみ知っています。

したがって、最終的には、最善のオプションが現在のソリューションのままです。別のプロセスを生成して、それに満足するだけです。そして、私は前の答えに同意します、あなたはそれぞれの場合のために別のコンソールアプリを書く必要はありません。静的メソッドの完全修飾名を渡して、コンソールアプリにアセンブリをロードさせ、タイプをロードして、メソッドを呼び出すだけです。実際には、AppDomainsで試したのとほぼ同じ方法で、かなりきれいにパッケージ化できます。 「RunInAnotherProcess」のようなメソッドを作成して、引数を調べ、そのメソッドから完全な型名とメソッド名を取得し(メソッドが静的であることを確認しながら)、コンソールアプリを生成して残りを実行できます。

75
Fyodor Soikin

多くのコンソールアプリケーションを作成する必要はありません。パラメーターとして完全修飾型名を受け取る単一のアプリケーションを作成できます。アプリケーションはそのタイプをロードして実行します。
すべてを小さなプロセスに分離することは、すべてのリソースを実際に廃棄するための最良の方法です。 アプリケーションドメイン はリソースを完全に破棄することはできませんが、プロセスは実行できます。

7
Shay Erlichmen

メインアプリケーションとサブアプリケーションの間で パイプを開く を検討しましたか?このようにして、標準出力を解析せずに、2つのアプリケーション間でより構造化された情報を渡すことができます。

2
user7116