应用程序域

注释

本文特定于 .NET Framework。 它不适用于 .NET 的较新版本实现,包括 .NET 6 及更高版本。

作系统和运行时环境通常提供应用程序之间的某种形式的隔离。 例如,Windows 使用进程隔离应用程序。 这种隔离是必要的,以确保在一个应用程序中运行的代码不能对其他不相关的应用程序产生负面影响。

应用程序域为安全、可靠性和版本控制以及卸载程序集提供隔离边界。 应用程序域通常由运行时主机创建,这些主机负责在运行应用程序之前启动公共语言运行时。

隔离应用程序的好处

从历史上看,进程边界用于隔离在同一台计算机上运行的应用程序。 每个应用程序都加载到单独的进程中,将应用程序与在同一台计算机上运行的其他应用程序隔离开来。

应用程序是隔离的,因为内存地址是进程相对的;从一个进程传递到另一个进程的内存指针不能在目标进程中以任何有意义的方式使用。 此外,不能在两个进程之间进行直接调用。 您必须代之以使用代理,它提供一定程度的间接性。

必须先通过验证过程传递托管代码才能运行(除非管理员已授予跳过验证的权限)。 验证过程确定代码是否可以尝试访问无效的内存地址,或者执行其他操作,从而导致其运行的进程无法正常运作。 通过验证测试的代码据说是类型安全的。 将代码验证为类型安全的功能使公共语言运行时能够以更低的性能成本提供与进程边界相同的隔离级别。

应用程序域提供了更安全且通用的处理单元,公共语言运行时可用于在应用程序之间提供隔离。 可以在单个进程中运行多个应用域,其隔离级别与独立进程中的隔离效果相同,但不会产生跨进程调用或在进程之间切换的额外开销。 在单个进程中运行多个应用程序的能力显著提高了服务器可伸缩性。

隔离应用程序对于应用程序安全性也很重要。 例如,可以在单个浏览器进程中运行来自多个 Web 应用程序的控件,这样控件就无法访问彼此的数据和资源。

应用程序域提供的隔离具有以下优势:

  • 一个应用程序中的错误不会影响其他应用程序。 由于类型安全代码无法导致内存错误,因此使用应用程序域可确保在一个域中运行的代码不会影响进程中的其他应用程序。

  • 可以在不停止整个进程的情况下停止单个应用程序。 使用应用程序域可以卸载在单个应用程序中运行的代码。

    注释

    不能卸载单个程序集或类型。 只能卸载整个域。

  • 在一个应用程序中运行的代码无法直接从另一个应用程序访问代码或资源。 公共语言运行时通过阻止在不同应用程序域中的对象之间直接调用来强制实施这种隔离。 在域之间传递的对象由代理复制或访问。 如果复制对象,则对对象的调用是本地的。 也就是说,调用方和所引用的对象都位于同一应用程序域中。 如果通过代理访问对象,则对对象的调用是远程的。 在这种情况下,调用方和所引用的对象位于不同的应用程序域中。 跨域调用使用与两个进程之间或两台计算机之间的调用相同的远程调用基础结构。 因此,被引用对象的元数据必须可以在这两个应用程序域中使用,以便方法调用能够正确地即时编译。 如果调用域无权访问被调用对象的元数据,编译可能会因为FileNotFoundException类型的异常而失败。 有关详细信息,请参阅 远程对象。 用于确定如何跨域访问对象的机制由该对象确定。 有关详细信息,请参阅 System.MarshalByRefObject

  • 代码的行为由运行代码的应用程序限定。 换句话说,应用程序域提供配置设置,例如应用程序版本策略、它访问的任何远程程序集的位置,以及有关查找加载到域中的程序集的位置的信息。

  • 授予代码的权限可由运行代码的应用程序域控制。

应用程序域和程序集

本部分介绍应用程序域和程序集之间的关系。 必须先将程序集加载到应用程序域中,然后才能执行它包含的代码。 运行典型应用程序会导致多个程序集加载到应用程序域中。

加载程序集的方式决定了其实时 (JIT) 编译代码是否可以由进程中的多个应用程序域共享,以及是否可以从进程中卸载程序集。

  • 如果程序集是非特定域加载的,共享同一安全授予集的所有应用程序域都可以共享相同的 JIT 编译代码,从而减少应用程序所需的内存。 但是,无法从进程中卸载程序集。

  • 如果程序集不是以非特定于域的形式进行加载,则它必须在加载的每个应用程序域中都是 JIT 编译的。 但是,通过卸载程序集加载的所有应用程序域,可以从进程中卸载程序集。

运行时主机确定在将运行时加载到进程中时是否将程序集作为非域性加载。 对于托管应用程序,请将属性 LoaderOptimizationAttribute 应用于进程的入口点方法,并指定关联 LoaderOptimization 枚举中的值。 对于托管公共语言运行时的非托管应用程序,请在调用 CorBindToRuntimeEx 函数 方法时指定相应的标志。

有三个选项可用于加载非特定域程序集:

  • LoaderOptimization.SingleDomain 不会将程序集作为非特定域加载,但 Mscorlib 除外,该程序集始终是非特定域的。 此设置称为单个域,因为当主机仅在进程中运行单个应用程序时,通常会使用它。

  • LoaderOptimization.MultiDomain 以非特定于域的形式加载所有程序集。 当进程中有多个应用程序域时,请使用此设置,所有这些域都运行相同的代码。

  • LoaderOptimization.MultiDomainHost 以非特定于域的形式加载强名称程序集(如果它们以及它们的所有依赖项都已在全局程序集缓存中安装)。 其他程序集会被加载,并在其加载的每个应用程序域中分别进行 JIT 编译,因此可从进程中卸载。 在以下情况下使用此设置:在同一进程中运行多个应用程序;或者有一些程序集被多个应用程序域共享,而另一些程序集需要从进程中卸载。

以下程序集不能共享 JIT 编译代码:使用 LoadFrom 类的 Assembly 方法加载到“加载源”上下文中的程序集,或者使用 Load 方法的重载(指定字节数组)从图像加载的程序集。

使用 Ngen.exe(本机映像生成器) 编译为本机代码的程序集可以在应用程序域之间共享,前提是首次将程序集加载到进程时处于非特定域状态。

仅当可以共享其所有依赖项时,才会共享包含应用程序入口点的程序集的 JIT 编译代码。

域中性程序集可以多次进行 JIT 编译。 例如,当两个应用程序域的安全授予集不同时,它们无法共享相同的 JIT 编译代码。 但是,JIT 编译程序集的每个副本都可以与其他具有相同授权集的应用程序域共享。

在决定是否将程序集作为非域性加载时,必须在减少内存使用和其他性能因素之间进行权衡。

  • 由于需要隔离程序集,对域中立程序集的静态数据和方法的访问速度较慢。 访问程序集的每个应用程序域都必须具有静态数据的单独副本,以防止对静态字段中对象的引用跨越域边界。 因此,运行时包含其他逻辑,用于将调用方定向到静态数据或方法的相应副本。 此额外逻辑会减慢调用速度。

  • 程序集的所有依赖项必须在程序集以域中性方式加载时定位并加载,因为无法以域中性方式加载的依赖项会阻止程序集被域中性加载。

应用程序域和线程

应用程序域形成隔离边界,用于安全、版本控制、可靠性和卸载托管代码。 线程是公共语言运行时用于执行代码的作系统构造。 在运行时,所有托管代码都加载到应用程序域中,并由一个或多个托管线程运行。

应用程序域和线程之间没有一对一关联。 多个线程可以在任意给定时间在单个应用程序域中执行,并且特定线程不局限于单个应用程序域。 也就是说,线程可以跨应用程序域边界;不会为每个应用程序域创建新线程。

在任何给定时间,每个线程都在应用程序域中执行。 在任何给定的应用程序域中,可能正在执行零个、一个或多个线程。 运行时跟踪哪些线程在哪个应用程序域中运行。 可以通过调用 Thread.GetDomain 该方法来查找线程随时正在执行的域。

应用程序域和文化

CultureInfo对象表示的文化与线程相关联。 可以使用该属性获取与当前正在执行的线程 CultureInfo.CurrentCulture 关联的区域性,并且可以使用该属性获取或设置与当前正在执行的线程 Thread.CurrentCulture 关联的区域性。 如果已使用 Thread.CurrentCulture 属性显式设置与线程关联的区域性,则当线程跨越应用程序域边界时,它将继续与该线程关联。 否则,在任何给定时间,与线程相关联的文化由线程执行所在的应用程序域中的CultureInfo.DefaultThreadCurrentCulture属性的值确定。

  • 如果该属性的值不是 null,则由该属性返回的区域性与线程(并因此由 Thread.CurrentCultureCultureInfo.CurrentCulture 属性返回)关联。

  • 如果该属性的值为 null,则当前系统文化信息与线程相关联。

使用应用程序域编程

应用程序域通常由运行时主机以编程方式创建和操作。 但是,有时应用程序程序可能还需要使用应用程序域。 例如,应用程序程序可以将应用程序组件加载到域中,以便能够卸载域(和组件),而无需停止整个应用程序。

AppDomain是应用程序域的编程接口。 此类包括创建和卸载域、在域中创建类型的实例以及注册各种通知(如应用程序域卸载)的方法。 下表列出了常用 AppDomain 方法。

AppDomain 方法 DESCRIPTION
CreateDomain 创建新的应用程序域。 建议使用此方法指定 AppDomainSetup 对象的重载形式。 这是设置新域的属性的首选方法,例如应用程序基目录或应用程序的根目录;域配置文件的位置;公共语言运行时用于将程序集加载到域中的搜索路径。
ExecuteAssemblyExecuteAssemblyByName 在应用程序域中执行程序集。 这是一个实例方法,因此可用于在引用的其他应用程序域中执行代码。
CreateInstanceAndUnwrap 在应用程序域中创建指定类型的实例,并返回代理。 使用此方法可避免将包含已创建类型的程序集加载到调用程序集中。
Unload 执行域的正常关闭。 在域中运行的所有线程已停止运行或离开域之前,应用程序域不会被卸载。

注释

公共语言运行时不支持全局方法的序列化,因此委托不能用于在其他应用程序域中执行全局方法。

公共语言运行时托管接口规范中所述的非托管接口还提供对应用程序域的访问权限。 运行时主机可以使用来自非托管代码的接口来创建和获取进程内应用程序域的访问权限。

COMPLUS_LoaderOptimization 环境变量

一个环境变量,用于设置可执行应用程序的默认加载程序优化策略。

语法

COMPLUS_LoaderOptimization = 1

注解

典型的应用程序将多个程序集加载到应用程序域中,然后才能执行它们包含的代码。

加载程序集的方式决定了其即时 (JIT) 编译的代码能否被进程中的多个应用程序域共享。

  • 如果程序集是非特定域加载的,共享同一安全授予集的所有应用程序域都可以共享相同的 JIT 编译代码。 这减少了应用程序所需的内存。

  • 如果程序集未以域中立方式加载,则必须在加载它的每个应用程序域中对其进行即时编译,并且加载程序不得在应用程序域之间共享内部资源。

当设置为 1 时,COMPLUS_LoaderOptimization 环境标志强制运行时主机以非域中立的方式加载所有程序集,这种方式称为 SingleDomain。 SingleDomain 不以非特定于域的形式加载任何程序集(Mscorlib 除外,它始终以非特定于域的形式加载)。 此设置称为单个域,因为当主机仅在进程中运行单个应用程序时,通常会使用它。

谨慎

COMPLUS_LoaderOptimization环境标志旨在用于诊断和测试方案。 启用标志可能会导致严重减慢和内存使用量增加。

代码示例

若要强制所有程序集不以域中性方式加载到 IISADMIN 服务,可以通过在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IISADMIN 键的环境多字符串值中追加 COMPLUS_LoaderOptimization=1 来实现。

Key = HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\IISADMIN
Name = Environment
Type = REG_MULTI_SZ
Value (to append) = COMPLUS_LoaderOptimization=1

另请参阅