用AppDomains替换Process.Start

50

背景

我有一个使用各种第三方DLL文件处理PDF文件的Windows服务。这些操作可能会使用大量系统资源,并且在发生错误时偶尔似乎会出现内存泄漏。这些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事件使单元测试通过。无论如何,我担心创建“工作”AppDomains可能无法解决我的问题。

您有什么想法吗?

代码

/// <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 ) )
    {
    }
}
3个回答

77
应用程序域和跨域交互非常微妙,因此在做任何事情之前,确保真正理解了工作原理... 嗯...可以说是“非标准” :-)
首先,你的流创建方法实际上在你的“默认”域上执行(惊喜!)。为什么呢?简单:你传递给AppDomain.DoCallBack的方法在一个AppDomainDelegateWrapper对象上定义,该对象存在于默认域上,因此它的方法在那里执行。MSDN没有提到这个小“特性”,但很容易检查:只需在AppDomainDelegateWrapper.Invoke中设置断点即可。
因此,基本上,你必须没有“包装器”对象。使用静态方法来调用DoCallBack的参数。
但是如何将“func”参数传递到其他域,以便你的静态方法可以接收它并执行呢?
最显然的方法是使用AppDomain.SetData,或者你可以编写自己的方法,但无论你怎样做,都存在另一个问题:如果“func”是一个非静态方法,则必须以某种方式将其定义的对象传递到其他应用程序域。它可以通过值传递(其中它被逐个字段复制)或通过引用传递(创建具有Remoting所有美丽的跨域对象引用)来传递。要执行前者,该类必须标记为[Serializable]属性。要执行后者,它必须继承自MarshalByRefObject。如果该类都不是,那么在尝试将对象传递到其他域时会抛出异常。然而,请记住,通过引用传递基本上破坏了整个想法,因为你的方法仍然会在对象存在的同一个域上调用 - 也就是默认的那个域。
总结以上段落,你只有两个选择:要么传递在标记为[Serializable]属性的类上定义的方法(请记住,对象将被复制),要么传递静态方法。我怀疑,对于你的目的,前者更合适。
但是万一你没有注意到,我想指出的是,你的第二个重载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"的方法,它将检查参数,获取其中的完全类型名称和方法名称(同时确保方法是静态的)并生成控制台应用程序,该程序将完成其余工作。


我发现这种技术在测试跨越不同进程的行为时非常有用。例如,如果IIS回收其应用程序池,并且您想在回收之前和之后测试组件行为。赞。 - Llyle
@vanslly完整的源代码示例是什么?“回收前后”是指什么? - Kiquenet
我有这段源代码:var dataSources = new List<Tuple<string, Func<IEnumerable>>> { Tuple.Create<string, Func<IEnumerable>>("TablaEvolucionVentasPolizas", () => { return listaPolizas; }), Tuple.Create<string, Func<IEnumerable>>("TablaEvolucionVentasPrimas", () => { return listaPrimas; }), Tuple.Create<string, Func<IEnumerable>>("TablaRamosVentas", () => { return listaRamos; }), }; - Kiquenet
为了在另一个AppDomain上执行委托,您可以使用[System.AppDomain.DoCallBack()][1]。 链接的MSDN页面有一个很好的例子。请注意,您只能使用类型为[CrossAppDomainDelegate][2]的委托。 - Kiquenet

7
你不必创建许多控制台应用程序,只需创建一个应用程序,并将完整的类型名称作为参数传递。该应用程序将加载该类型并执行它。
将所有内容分离成小进程是真正处理所有资源的最佳方法。应用程序域无法进行完全的资源处理,但进程可以。

什么资源不会被 AppDomain 卸载而回收? - Tinister

2

您是否考虑过在主应用程序和子应用程序之间打开一个管道?这样,您可以在两个应用程序之间传递更加结构化的信息,而无需解析标准输出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接