22.2.4 应用程序域

在介绍什么是应用程序域之前,先了解一下什么是“进程”和“线程”。

进程是操作系统最基本、最重要的概念之一。程序是指令的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。当一个应用程序开始执行,操作系统就会为应用程序创建一个进程,每一个进程都有它自己的内存“沙盒”。所谓的“沙盒”,指的是虚拟地址空间(下文简称“虚拟内存”),虚拟内存能够映射到物理内存,这种映射由操作系统内核来管理,并可以被处理器访问。操作系统为每个进程都分配的虚拟内存都是私有的,运行在其他进程中的应用程序不能写入另一个进程的内存,这确保了一个进程中的错误不会影响到其他的进程。讲到这里,大家可能会发现,进程在这里实际上扮演的是应用程序间的边界的角色——一个安全而独立的边界。换句话说,进程是操作系统用于隔离众多应用程序的一种手段。

需要说明的是,并非一个应用程序仅能对应一个进程。事实上,一个应用程序可以对应多个进程。例如现在的浏览器纷纷采用多进程的解决方案,这可以避免在一个标签死锁以后,不至于导致整个浏览器停止响应,或者其他页面也死锁。

线程是进程中的基本执行单元,线程并不拥有系统资源,它和其他线程共享进程拥有的所有资源。一个线程拥有自己的执行堆栈和程序计数器。当一个进程创建后,操作系统就会为该进程分配一个默认的线程,这个线程又叫做“主线程”。除了“主线程”以外,线程还可以创建更多的线程。只有一个线程的应用程序是线程安全的,但用户体验并不好。例如,当主线程在忙于一个非常耗时的工作时,用户的任何操作都无法被即时处理,整个应用程序仿佛处于假死状态,让人非常恼火。同一进程中的多个线程可以并发运行,这称为多线程。因此为了具有更好的用户体验或工作效率,应用程序往往会使用多个线程,至少将工作线程和响应界面操作的主线程分开,这样用户至少可以对于非常耗时的工作采取“取消”或者进行其他的操作,而不是一味等待。但线程并非越多越好,有时候线程过多反而导致性能下降,特别是运行在一个单核的CPU上,因为在任意时刻只能运行一个线程,因此这些线程的运行时间就需要操作系统负责调度,当操作系统为一个线程分配的执行时间(时间片)用完后,该线程就会被挂起,等待其他线程执行完毕后重新激活,然后继续执行,这样不断地来回切换也需要花费时间。

.NET引入了一个新的概念——“应用程序域(AppDomain)”,可看成是位于一个进程空间的逻辑分区,这是.NET应用程序的新边界。CLR使用应用程序域提供应用程序之间的隔离,如图22-6所示。应用程序域之所以可以提供与进程边界一样大的隔离级别,是因为托管代码在运行前必须先通过一个验证过程(除非管理员已授权跳过该验证),此验证过程将验证以下内容:

❑这些代码是否会尝试访问无效的内存地址?

❑是否会尝试执行某些导致进程(该代码运行时所在的进程)无法正常进行的其他操作?

通过这些验证的代码将被认为是类型安全的。由于CLR能够验证代码是否为类型安全的代码,所以它不但提供了与进程边界同样的隔离级别,而且其性能开销还要低得多。

22.2.4 应用程序域 - 图1

图 22-6 应用程序域示意图

应用程序域提供的隔离具有如下优点:

❑一个应用程序域中的应用程序中出现的错误不会影响其他应用程序域中的应用程序。因为类型安全的代码不会导致内存错误,所以使用应用程序域可以确保在一个域中运行的代码不会影响进程中的其他域中的应用程序。

❑能够在不停止整个进程的情况下停止单个应用程序。使用应用程序域还可以卸载在单个应用程序中运行的代码,但不能卸载单个程序集或类型,只能卸载整个域。

❑在一个应用程序域中运行的代码不能直接访问其他应用程序域中的代码或资源。为了强制实施此隔离,CLR不允许在不同应用程序域中的对象之间进行直接调用。但如果真的需要进行这种“域间通信”,也是有办法的。例如,要在各域之间传递对象,可以复制这些对象,或通过代理访问这些对象。如果复制对象,那么对该对象的调用就转换为本地调用。也就是说,调用方和被引用的对象此时位于同一个应用程序域中;而如果通过代理访问对象,那么对该对象的调用则为远程调用。在这种情况下,调用方和被引用的对象位于不同的应用程序域中。

❑向代码授予的权限可以由代码运行所在的应用程序域来控制。

另外,运行.NET应用程序时,程序集需要加载到应用程序域才可以运行,这同传统的Win32应用程序直接加载到进程中是不同的。应用程序域使应用程序以及应用程序的数据彼此分离,有助于提高安全性。