使用AppDomain替​​换Process.Start

背景

我有一个Windows服务使用各种第三方DLL来执行PDF文件的工作。 这些操作可以使用相当多的系统资源,偶尔在发生错误时似乎会遭受内存泄漏。 这些DLL是托pipe在其他非托pipeDLL周围的包装器。

当前解决scheme

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

我想将这个相同的逻辑应用到我的应用程序中使用这些DLL的其他地方。 然而,我并不是非常兴奋地为我的解决scheme添加更多的控制台项目,编写更多的调用Process.Start()的样板代码,并分析控制台应用程序的输出。

新解决scheme

一个优雅的替代专用控制台应用程序和Process.Start()似乎是使用AppDomains,像这样: http : //blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx 。

我在我的应用程序中实现了类似的代码,但是unit testing并没有很好的前景。 我创build一个FileStream到一个单独的AppDomain中的testing文件,但不要处置它。 然后我尝试在主域中创build另一个FileStream,并由于未释放的文件locking而失败。

有趣的是,向工作域添加一个空的DomainUnload事件使得unit testing通过。 无论如何,我担心的是,也许创build“工人”AppDomain不会解决我的问题。

思考?

代码

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

unit testing

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

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

首先,您的stream创build方法实际上在您的“默认”域执行(惊喜!)。 为什么? 简单:你传入AppDomain.DoCallBack的方法是在一个AppDomainDelegateWrapper对象上定义的,并且该对象存在于你的默认域中,所以这就是它的方法得到执行的地方。 MSDN不会说这个小“function”,但很容易检查:只需在AppDomainDelegateWrapper.Invoke设置一个断点。

所以,基本上,你不得不做一个“包装”对象。 对于DoCallBack的参数使用静态方法。

但是,你如何将你的“func”parameter passing给其他域呢,这样你的静态方法就可以把它提取出来并执行了?

最明显的方法是使用AppDomain.SetData ,或者你可以推出你自己的,但是无论你如何去做,还有另一个问题:如果“func”是一个非静态的方法,那么它定义的对象必须以某种方式传递到另一个AppDomain。 它可以通过值来传递(而是被复制,逐个字段)或通过引用(创build具有Remoting所有美感的跨域对象引用)。 要做前者,该类必须用[Serializable]属性标记。 要做后者,它必须从MarshalByRefObjectinheritance。 如果类不是,则尝试将对象传递给其他域时将引发exception。 但请记住,通过引用几乎杀死了整个想法,因为你的方法仍然会在对象所在的同一个域上被调用 – 也就是默认的方法。

结束上面的段落,你剩下两个select:或者传递一个在[Serializable]属性标记的类上定义的方法(并且记住该对象将被复制),或者传递一个静态方法。 我怀疑,为了你的目的,你需要前者。

为了防止您的注意,我想指出, RunInAppDomain (需要Action )的第二个重载传递了一个未标记为[Serializable]的类上定义的方法。 在那里看不到任何课程? 您不必:使用包含绑定variables的匿名委托,编译器将为您创build一个。 而编译器并不打算标记自动生成的类[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 ); } 

如果你还在我身边,我很欣赏:-)

现在,花了那么多时间来修正这个机制之后,我要告诉你,无论如何,这是无意义的。

事情是,AppDomain不会帮助你达到你的目的。 他们只关心托pipe对象,而非托pipe代码可能会泄漏并使其崩溃。 非托pipe代码甚至不知道有像appdomains这样的东西。 它只知道进程。

所以,最后,你最好的select仍然是你现在的解决scheme:只是产生另一个过程,并为此感到高兴。 而且,我会同意以前的答案,您不必为每个案例编写另一个控制台应用程序。 只需传递一个静态方法的完全限定名称,然后让控制台应用程序加载你的程序集,加载你的types并调用方法。 实际上,您可以使用与AppDomain一样的方式将其打包得很整洁。 您可以创build一个名为“RunInAnotherProcess”的方法,它将检查参数,从中获取完整的types名称和方法名称(同时确保该方法是静态的)并产生控制台应用程序,其余部分将完成。

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

您是否考虑在主应用程序和子应用程序之间打开一个pipe道 ? 通过这种方式,您可以在两个应用程序之间传递更多结构化信息,而无需parsing标准输出