【问题标题】:Replacing Process.Start with AppDomains替换 Process.Start 与 AppDomains
【发布时间】:2010-12-03 09:16:24
【问题描述】:

背景

我有一个 Windows 服务,它使用各种第三方 DLL 来处理 PDF 文件。这些操作可能会使用相当多的系统资源,并且在发生错误时偶尔会出现内存泄漏。 DLL 是其他非托管 DLL 的托管包装器。

当前解决方案

在一种情况下,我已经通过在专用控制台应用程序中包装对其中一个 DLL 的调用并通过 Process.Start() 调用该应用程序来缓解此问题。如果操作失败并且存在内存泄漏或未释放的文件句柄,这并不重要。该过程将结束,操作系统将恢复句柄。

我想将同样的逻辑应用到我的应用程序中使用这些 DLL 的其他地方。但是,我对在我的解决方案中添加更多控制台项目并编写更多样板代码来调用 Process.Start() 并解析控制台应用程序的输出并不感到非常兴奋。

新解决方案

专用控制台应用程序和 Process.Start() 的一个优雅替代方案似乎是使用 AppDomains,如下所示:http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx

我已经在我的应用程序中实现了类似的代码,但单元测试并不乐观。我在单独的 AppDomain 中为测试文件创建了一个 FileStream,但不释放它。然后我尝试在主域中创建另一个 FileStream,但由于未释放的文件锁而失败。

有趣的是,向工作域添加一个空的 DomainUnload 事件可以使单元测试通过。无论如何,我担心创建“worker” 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 ) )
    {
    }
}

【问题讨论】:

    标签: c# appdomain .net-3.5 process.start


    【解决方案1】:

    应用程序域和跨域交互是一件非常简单的事情,所以在做任何事情之前应该确保他真正了解事物的工作原理......嗯......让我们说“非标准”:-)

    首先,您的流创建方法实际上在您的“默认”域上执行(惊喜!)。为什么?很简单:您传递给AppDomain.DoCallBack 的方法是在AppDomainDelegateWrapper 对象上定义的,并且该对象存在于您的默认域中,因此这是执行其方法的地方。 MSDN 没有说明这个小“功能”,但检查起来很容易:只需在 AppDomainDelegateWrapper.Invoke 中设置断点即可。

    所以,基本上,你必须在没有“包装”对象的情况下凑合着用。对 DoCallBack 的参数使用静态方法。

    但是你如何将你的“func”参数传递给另一个域,以便你的静态方法可以获取并执行它?

    最明显的方法是使用AppDomain.SetData,或者你可以自己滚动,但是不管你怎么做,还有一个问题:如果“func”是一个非静态方法,那么对象它的定义必须以某种方式传递到另一个应用程序域。它可以通过值(而它被逐个字段地复制)或通过引用(创建具有 Remoting 的所有美感的跨域对象引用)传递。要做到这一点,必须用[Serializable] 属性标记该类。要做到后者,它必须从MarshalByRefObject 继承。如果类都不是,则在尝试将对象传递到另一个域时将引发异常。但是请记住,通过引用传递几乎会扼杀整个想法,因为您的方法仍将在对象所在的同一域上调用 - 即默认域。

    结束上述段落,您有两个选择:要么传递一个定义在标有[Serializable] 属性的类上的方法(并记住该对象将被复制),要么传递一个静态方法。我怀疑,出于您的目的,您将需要前者。

    以防万一它没有引起您的注意,我想指出您的 RunInAppDomain 的第二个重载(采用 Action 的那个)传递了一个在未标记为 @987654333 的类上定义的方法@。那里没有看到任何课程?您不必这样做:对于包含绑定变量的匿名委托,编译器将为您创建一个。恰好编译器不会费心标记自动生成的类[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”之类的方法,它将检查参数,从中获取完整的类型名称和方法名称(同时确保方法是静态的)并生成控制台应用程序,剩下的工作将由它完成。

    【讨论】:

    • 我发现这种技术在测试跨越不同进程的行为时很有用。例如,如果 IIS 回收其应用程序池,并且您希望在回收之前和之后测试组件行为。荣誉。
    • @vanslly 完整的源代码示例呢? before and after the recycle
    • 我有这个源代码:var dataSources = new List&lt;Tuple&lt;string, Func&lt;IEnumerable&gt;&gt;&gt; { Tuple.Create&lt;string, Func&lt;IEnumerable&gt;&gt;("TablaEvolucionVentasPolizas", () =&gt; { return listaPolizas; }), Tuple.Create&lt;string, Func&lt;IEnumerable&gt;&gt;("TablaEvolucionVentasPrimas", () =&gt; { return listaPrimas; }), Tuple.Create&lt;string, Func&lt;IEnumerable&gt;&gt;("TablaRamosVentas", () =&gt; { return listaRamos; }), };
    • stackoverflow.com/a/14320090/206730 要在另一个 AppDomain 上执行委托,您可以使用 [System.AppDomain.DoCallBack()][1]。链接的 MSDN 页面有一个很好的例子。请注意,您只能使用 [CrossAppDomainDelegate][2] 类型的委托。 [1]:msdn.microsoft.com/en-us/library/… [2]:msdn.microsoft.com/en-us/library/…
    【解决方案2】:

    您不必创建许多控制台应用程序,您可以创建一个应用程序,该应用程序将接收完整的限定类型名称作为参数。应用程序将加载该类型并执行它。
    将所有东西分离成微小的进程是真正处置所有资源的最佳方法。 application domain 不能进行全部资源处理,但进程可以。

    【讨论】:

    • AppDomain卸载没有回收哪些资源?
    【解决方案3】:

    你在主应用和子应用之间考虑过opening a pipe吗?这样您就可以在两个应用程序之间传递更多结构化信息,而无需解析标准输出。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-20
      • 1970-01-01
      • 1970-01-01
      • 2011-09-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多