50

背景

我有一个 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 事件会使单元测试通过。无论如何,我担心创建“工人”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 ) )
    {
    }
}
4

3 回答 3

76

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

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

因此,基本上,您必须在没有“包装器”对象的情况下凑合。对 DoCallBack 的参数使用静态方法。

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

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

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

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

于 2009-10-02T18:44:31.147 回答
7

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

于 2009-10-02T16:35:07.803 回答
2

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

于 2009-10-02T16:43:55.977 回答