用 AppDomains 替换 Process.Start [英] Replacing Process.Start with AppDomains

查看:36
本文介绍了用 AppDomains 替换 Process.Start的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

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

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.

当前解决方案

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

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.

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

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.

新解决方案

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

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.

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

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.

有趣的是,向工作域添加一个空的 DomainUnload 事件会使单元测试通过.无论如何,我担心创建worker"AppDomains 可能无法解决我的问题.

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.

想法?

代码

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

推荐答案

应用程序域和跨域交互是一件非常细小的事情,所以在做任何事情之前应该确保他真的了解事情的工作原理......嗯... 比方说,非标准":-)

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" :-)

首先,您的流创建方法实际上是在您的默认"域上执行的(出乎意料!).为什么?简单:您传递给 AppDomain.DoCallBack 的方法是在 AppDomainDelegateWrapper 对象上定义的,并且该对象存在于您的默认域中,因此它的方法就在那里执行.MSDN 没有提及这个小功能",但它很容易检查:只需在 AppDomainDelegateWrapper.Invoke 中设置一个断点.

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.

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

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

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

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

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

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.

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

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.

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

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 :-)

说了这么多(很多话,不是吗?:-),并假设您发誓不传递任何非静态和非[Serializable] 方法,这里是您的新 RunInAppDomain 方法:

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.

问题是,AppDomains 不会为您的目的提供帮助.它们只处理托管对象,而非托管代码可以随心所欲地泄漏和崩溃.非托管代码甚至不知道有 appdomains 之类的东西.它只知道进程.

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.

因此,最后,您最好的选择仍然是您当前的解决方案:只需生成另一个进程并对此感到高兴.而且,我同意之前的答案,您不必为每种情况编写另一个控制台应用程序.只需传递静态方法的完全限定名称,然后让控制台应用程序加载您的程序集、加载您的类型并调用该方法.实际上,您可以使用与使用 AppDomains 非常相似的方式将其打包得非常整齐.您可以创建一个名为RunInAnotherProcess"的方法,该方法将检查参数,从中获取完整的类型名称和方法名称(同时确保该方法是静态的)并生成控制台应用程序,它将完成剩下的工作.

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.

这篇关于用 AppDomains 替换 Process.Start的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆