AppDomain,处理异常

16

我正在开发一个大型应用程序,它由许多较小的插件/应用程序组成。

它们不足以成为完整的进程,但又太小而无法在同一个进程的线程下运行,并且我希望它以插件为基础。 如果可用该插件的新版本,则应卸载、更新并重新启动。

在寻找解决方案时,我发现了“应用程序域”这个神奇的词语,我引用一下:

“使用应用程序域来隔离可能会导致进程崩溃的任务。如果正在执行任务的AppDomain的状态变得不稳定,可以卸载该AppDomain而不影响进程。当进程必须长时间运行而无需重启时,这很重要。您还可以使用应用程序域来隔离不应共享数据的任务。”

因此,那正是我想要的。然而,我猜他们所说的“状态变得不稳定”与我的观点不同。我考虑的是一个插件抛出异常的问题,无论出于什么原因。我希望能够捕获、发送电子邮件、卸载和重新启动(如果可能)。

因此,我创建了一个应用程序,启动后搜索其文件夹中的所有.dll文件。检查dll是否由插件组成。为该插件创建一个新的AppDomain,一切都加载完成后,它将启动每个插件。(其中每个插件可以由多个线程组成,相互和谐共存)。

因此,我还添加了一个超时,5秒后会引发新的异常(); 在AppDomain上添加了一个UnhandledException事件来处理它。但是,它捕获了它,并且在捕获之后,仍然“崩溃”整个进程,包括所有额外的子AppDomains。

但是它在引用中明确指出“隔离可能会导致进程崩溃的任务”。那么我是否遗漏了重要的东西?我的观点是否有误?


你看过MEF了吗? - user47589
@Inuyasha 没有,我明天回到工作岗位后会做的。 - Daan Timmer
你是如何“启动每个插件”的?如果你使用了创建实例和解包装方法中的一个,那么异常会返回到调用域,你需要捕获它。 - Mike Zboray
“太小了,无法在线程中运行”为什么组件的大小会影响它呢?如果需要多个线程,您可以直接这样做,没有必要使用AppDomains。 (似乎您有其他使用它们的原因,但我不认为这是其中之一。) - svick
@Inuyasha,我刚刚看了一下MEF和一些示例,它似乎不是我可以用于这个项目的东西。由于每个插件都监听自己的Socket并通过Socket等待数据,而不是通过“主机”,因此这里是我们项目的更精确描述:https://dev59.com/21rUa4cB1Zd3GeqPiUvd#7072103(请参见编辑)。 - Daan Timmer
当处理UnhandledException事件时,你会在eventargs上得到一个名为IsTerminating的属性。我不知道它的值是由什么决定的,但如果它为true,进程将崩溃并退出。 - Peter
4个回答

18
自 .NET 2.0 起,未处理的异常会使进程崩溃。来自 AppDomain.UnhandledException 事件文档的描述:

此事件提供未捕获异常的通知。它允许应用程序在系统默认处理程序报告异常给用户并终止应用程序之前记录有关异常的信息。

对于 AppDomain.FirstChanceException 也是同样的情况:

此事件仅供通知。处理此事件不会处理异常或以任何方式影响后续异常处理。

你需要考虑如何处理异常,就像在正常的应用程序中一样。仅使用 AppDomains 是无法解决问题的。如果给定的 AppDomain 中未处理异常,则会在调用 AppDomain 中重新抛出该异常,直到它被处理或导致进程崩溃。可以 perfectly fine 处理 某些 异常并防止其崩溃您的进程。
AppDomain 是装配和内存(而非线程)的逻辑容器。AppDomain 的隔离意味着:
  • 在域A中创建的对象不能直接被域B访问(需要进行编组)。这使得域A可以被卸载而不影响域B中的任何内容。这些对象将在“拥有”域被卸载时自动删除。

  • 使用AppDomain可以自动卸载程序集。这是从进程中卸载托管dll的唯一方法。这对于DLL热插拔非常有用。

  • AppDomain安全权限和配置可以与其他AppDomains隔离。当您加载不受信任的第三方代码时,这可能会有所帮助。它还允许您覆盖如何加载程序集(版本绑定、影子复制等)。

使用AppDomain最常见的原因是运行不受信任的第三方代码。或者您有非托管代码并且想要托管CLR或需要dll热插拔。我认为在CLR托管场景下,当第三方代码抛出未处理的异常时,可以避免进程崩溃

此外,您可能想要查看System.Addin或MEF,而不是自己编写基础架构。


我在这里的一个相关问题中添加了更多关于我们/我的项目的信息:https://dev59.com/21rUa4cB1Zd3GeqPiUvd,请查看编辑1和编辑2。 - Daan Timmer
@Daan:看一下这个 https://dev59.com/21rUa4cB1Zd3GeqPiUvd#7078779 - Dmitry

7
处理未处理异常有两个问题,AppDomain 只解决其中一个。你需要处理另一个问题。
首先是好消息。当你处理异常时,你必须将程序状态还原到异常发生前的状态。你通常有一堆 catch 和 finally 子句来撤消代码执行的状态变化。当然,这并不简单。但如果异常未被处理,则完全不可能。你不知道哪些内容被修改了,也不知道如何还原它们。AppDomain 以出色的表现处理了这个非常困难的问题。你卸载它,留下的任何状态都会消失。没有更多的垃圾收集堆栈,没有更多的加载器堆栈(静态)。整个操作都会重置为创建 AppDomain 之前的状态。
这很棒。但还有另一个问题也很难处理。你的程序被要求执行某项工作。线程开始执行该任务。但突然间出现了错误。第一个大问题:线程挂了。如果你的程序一开始只有一个线程,那就糟糕了。没有剩余的线程,程序终止。AppDomain 先卸载也很好,但实际上并没有什么区别,因为无论如何它都会被卸载。
第二个大问题:这项工作真的相当重要。但它没有完成。这很重要,比如平衡企业的损益表。这没有完成,某个人必须负责,因为不平衡的报表会让很多人非常不满意。
如何解决这个问题?
只有在一些特定的场景下才可以接受这种情况。服务器场景。有人要求它执行某项任务,服务器返回“无法完成,请联系系统管理员”。ASP.NET 和 SQL Server 的工作方式。他们使用 AppDomains 来保持服务器的稳定性。并且有系统管理员来处理问题。你需要创建这样的支持系统,以使 AppDomains 为你工作。

另一个需要注意的问题是,如果线程在新的应用程序域中生成一个新的子线程,并且这个新线程抛出异常,则无法恢复。进程将会崩溃(尽管您仍然可以收到事件)。因此,即使使用了try catch也可能出现插件导致进程崩溃的情况。最完善的解决方案是将其托管在一个新的进程中,但是当然它也有自己的缺点。 - Frank Q.

5

如果有人考虑(我曾经也在那里)主要使用应用程序域来保证应用程序的稳定性,我会在这个主题上添加一些额外的信息:

几年前,System.AddIn团队发布了一篇非常有趣的博客文章。使用AppDomain隔离检测插件失败

它解释说,只有使用进程外插件才能保证宿主的稳定性。更具体地说:

从CLR v2.0开始,子线程上的未处理异常现在将导致整个进程被关闭,因此宿主无法完全恢复。

所以他们建议订阅AppDomain.UnhandledException,在应用程序崩溃之前存储信息(日志、数据库等)关于谁导致了这个异常。然后下次您的应用程序启动时,使用此信息来保护您的应用程序。也许您不加载插件,或者通知用户并让用户决定。(Microsoft Office 应用程序采用了这种方法,并禁用了崩溃主机的插件。然后您必须自己重新启用它们。)
他们还发表了另一篇博客文章,展示了如何在主机运行在另一个主机中的情况下甚至执行此操作(IIS、WAS 等)。更多关于从托管插件记录未处理异常的内容
尽管这两篇文章都集中在System.AddIn周围,但它们包含了任何试图增加其插件感知应用程序的稳定性的人有用的信息。

3

AppDomain通常用于卸载程序集(就像您建议的那样)以及控制启动参数,例如.NET访问级别、配置等。如果您真的想要“隔离”,那么最好的选择始终是工作进程;但是,这需要更多的工作。

我在几个项目中都做了相当多的工作。简单来说,我们使用Google ProtoBuffersJon Skeet的移植版)在托管的Windows LRPC库上进行大部分通信。对于工作进程管理,我们严重依赖命名事件,我最近发布了一个跨进程事件库,专门用于此目的。


1
“worker process”是什么意思?是指哪个C#类?Thread、Task还是BackgroundWorker? - Martin Meeser
他的意思是完全不同的Windows进程。比如一个新的MyApp.exe实例。 - Andrei Rînea
单独的进程是最安全的。这样,您可以确保插件不会使您的进程崩溃,因为即使插件在单独的应用程序域中,它仍然可能使您的进程崩溃。 - Frank Q.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接