首页 文章

用AppDomains替换Process.Start

提问于
浏览
49

Background

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

Current Solution

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

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

New Solution

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

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

有趣的是,将空DomainUnload事件添加到工作程序域会使单元测试通过 . 无论如何,我担心可能创建“ Worker ”AppDomains无法解决我的问题 .

思考?

The Code

/// <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 () );
    }
}

The unit test

[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 ) )
    {
    }
}

3 回答

  • 74

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

    首先,您的流创建方法实际上在您的"default"域上执行(惊喜惊喜!) . 为什么?简单:传入 AppDomain.DoCallBack 的方法是在 AppDomainDelegateWrapper 对象上定义的,并且该对象存在于您的默认域中,因此这是执行其方法的位置 . MSDN不容易检查:只需在 AppDomainDelegateWrapper.Invoke 中设置断点即可 .

    所以,基本上,你必须没有“包装”对象 . 对DoCallBack的参数使用静态方法 .

    但是,如何将“func”参数传递到另一个域,以便静态方法可以将其拾取并执行?

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

    总结上面的段落,你有两个选择:传递一个在标有 [Serializable] 属性的类上定义的方法(并记住该对象将被复制),或者传递一个静态方法 . 我怀疑,为了你的目的,你需要前者 .

    为了防止它引起你的注意,我想指出你的第二次重载 RunInAppDomain (需要 Action 的那个)传递一个在未标记为 [Serializable] 的类上定义的方法 . Don 't see any class there? You don' t必须:对于包含绑定变量的匿名委托,编译器将为您创建一个 . 而且只是编译器没有打算标记自动生成的类 [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”的方法,它将检查参数,从中获取完整的类型名称和方法名称(同时确保方法是静态的)并生成控制台应用程序,其余的将完成 .

  • 7

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

  • 1

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

相关问题