背景

我有一个 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”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 ) )
    {
    }
}
有帮助吗?

解决方案

应用程序域和跨域交互是一件非常薄弱的​​事情,所以在做任何事情之前应该确保他真正理解事物是如何工作的......嗯...比方说,“非标准”:-)

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

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

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

最明显的方法是使用 AppDomain.SetData, ,或者你可以自己推出,但无论你具体如何做,都会遇到另一个问题:如果“func”是非静态方法,则定义它的对象必须以某种方式传递到另一个应用程序域中。它可以通过值传递(而它是逐个字段复制的)或通过引用传递(创建具有远程处理所有优点的跨域对象引用)。要做到前者,该类必须标有 [Serializable] 属性。要做到后者,它必须继承自 MarshalByRefObject. 。如果该类两者都不是,则在尝试将该对象传递到另一个域时将引发异常。但请记住,通过引用传递几乎破坏了整个想法,因为您的方法仍将在对象所在的同一域(即默认域)上调用。

总结上一段,您有两个选择:要么传递在标有 a 的类上定义的方法 [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”的方法,它将检查参数,从中获取完整的类型名称和方法名称(同时确保该方法是静态的)并生成控制台应用程序,该应用程序将完成其余的工作。

其他提示

您不必创建许多控制台应用程序,您可以创建将接受作为参数的完全限定类型名称的单个应用程序。该应用程序将加载该类型并执行它。结果 一切分离成微小的过程是真正配置所有资源的最佳方法。一个应用领域不能做到完全的资源配置,而是一个过程。

你考虑开口之间的管主要应用和副应用?这样你可以通过在两个应用程序之间的更结构化的信息,而无需解析标准输出。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top