用 AppDomains 替换 Process.Start

发布于 2024-08-06 16:53:19 字数 3314 浏览 8 评论 0原文

背景

我有一个 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 ) )
    {
    }
}

Background

I have a Windows service that uses various third-party DLLs to perform work on PDF files. These operations can use quite a bit of system resources, and occasionally seem to suffer from memory leaks when errors occur. The DLLs are managed wrappers around other unmanaged DLLs.

Current Solution

I'm already mitigating this issue in one case by wrapping a call to one of the DLLs in a dedicated console app and calling that app via Process.Start(). If the operation fails and there are memory leaks or unreleased file handles, it doesn't really matter. The process will end and the OS will recover the handles.

I'd like to apply this same logic to the other places in my app that use these DLLs. However, I'm not terribly excited about adding more console projects to my solution, and writing even more boiler-plate code that calls Process.Start() and parses the output of the console apps.

New Solution

An elegant alternative to dedicated console apps and Process.Start() seems to be the use of AppDomains, like this: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx

I've implemented similar code in my application, but the unit tests have not been promising. I create a FileStream to a test file in a separate AppDomain, but don't dispose it. I then attempt to create another FileStream in the main domain, and it fails due to the unreleased file lock.

Interestingly, adding an empty DomainUnload event to the worker domain makes the unit test pass. Regardless, I'm concerned that maybe creating "worker" AppDomains won't solve my problem.

Thoughts?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

失而复得 2024-08-13 16:53:19

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

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

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

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

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

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

以防万一它逃过了您的注意,我想指出您的 RunInAppDomain 的第二个重载(采用 Action 的重载)传递了在类上定义的方法未标记为[Serialized]。没有看到那里有任何课程吗?您不必这样做:对于包含绑定变量的匿名委托,编译器将为您创建一个。碰巧的是,编译器懒得标记自动生成的类[Serialized]。不幸的是,但这就是生活:-)

说了这么多(很多话,不是吗?:-),并假设你发誓不传递任何非静态和非[Serialized] 方法,这是您的新 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”的方法,它将检查参数,从中获取完整的类型名称和方法名称(同时确保该方法是静态的)并生成控制台应用程序,该应用程序将完成其余的工作。

Application domains and cross-domain interaction is a very thin matter, so one should make sure he really understands how thing work before doing anything... Mmm... Let's say, "non-standard" :-)

First of all, your stream-creating method actually executes on your "default" domain (surprise-surprise!). Why? Simple: the method that you pass into AppDomain.DoCallBack is defined on an AppDomainDelegateWrapper object, and that object exists on your default domain, so that is where its method gets executed. MSDN doesn't say about this little "feature", but it's easy enough to check: just set a breakpoint in AppDomainDelegateWrapper.Invoke.

So, basically, you have to make do without a "wrapper" object. Use static method for DoCallBack's argument.

But how do you pass your "func" argument into the other domain so that your static method can pick it up and execute?

The most evident way is to use AppDomain.SetData, or you can roll your own, but regardless of how exactly you do it, there is another problem: if "func" is a non-static method, then the object that it's defined on must be somehow passed into the other appdomain. It may be passed either by value (whereas it gets copied, field by field) or by reference (creating a cross-domain object reference with all the beauty of Remoting). To do former, the class has to be marked with a [Serializable] attribute. To do latter, it has to inherit from MarshalByRefObject. If the class is neither, an exception will be thrown upon attempt to pass the object to the other domain. Keep in mind, though, that passing by reference pretty much kills the whole idea, because your method will still be called on the same domain that the object exists on - that is, the default one.

Concluding the above paragraph, you are left with two options: either pass a method defined on a class marked with a [Serializable] attribute (and keep in mind that the object will be copied), or pass a static method. I suspect that, for your purposes, you will need the former.

And just in case it has escaped your attention, I would like to point out that your second overload of RunInAppDomain (the one that takes Action) passes a method defined on a class that isn't marked [Serializable]. Don't see any class there? You don't have to: with anonymous delegates containing bound variables, the compiler will create one for you. And it just so happens that the compiler doesn't bother to mark that autogenerated class [Serializable]. Unfortunate, but this is life :-)

Having said all that (a lot of words, isn't it? :-), and assuming your vow not to pass any non-static and non-[Serializable] methods, here are your new RunInAppDomain methods:

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

If you're still with me, I appreciate :-)

Now, after spending so much time on fixing that mechanism, I am going to tell you that is was purposeless anyway.

The thing is, AppDomains won't help you for your purposes. They only take care of managed objects, while unmanaged code can leak and crash all it wants. Unmanaged code doesn't even know there are such things as appdomains. It only knows about processes.

So, in the end, your best option remains your current solution: just spawn another process and be happy about it. And, I would agree with the previous answers, you don't have to write another console app for each case. Just pass a fully qualified name of a static method, and have the console app load your assembly, load your type, and invoke the method. You can actually package it pretty neatly in a very much the same way as you tried with AppDomains. You can create a method called something like "RunInAnotherProcess", which will examine the argument, get the full type name and method name out of it (while making sure the method is static) and spawn the console app, which will do the rest.

满地尘埃落定 2024-08-13 16:53:19

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

You don't have to create many console applications, you can create a single application that will receive as parameter the full qualified type name. The application will load that type and execute it.
Separating everything into tiny processes is the best method to really dispose all the resources. An application domain cannot do full resources disposing, but a process can.

饮湿 2024-08-13 16:53:19

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

Have you considered opening a pipe between the main application and the sub applications? This way you could pass more structured information between the two applications without parsing standard output.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文