原作者: George P. Alexander Jr. (Software Engineer) 原文发表日期: 9/1/2005
天哪, .NET Framework的CLR真是巧妙呢! 随着越来越多的对.NET底层编程的了解, 一些诸如架构, 处理过程的复杂难懂的细节完全地让我叹服. 所以呢, 再次错过我们之前忽视的细节之美是不可能的了. 有个与CLR肩并肩协同工作的一个核心组件, 叫做AppDomain, 作为.NET Framework的一部分的AppDomain是一个微软引入的非常酷的概念.
为了更好的理解.NET的AppDomain和AppDomain是如何影响我们创建并在其上工作的程序的, 还是从头说起比较好. 那么让我们从在应用程序中点击一个按钮开始. 无论何时我们启动一个应用程序, 我们实际上启动了一个Win32的进程, 并且在这个进程中运行我们的程序. 这些进程使用那些诸如内存, 对象, 内核还有等等等等的资源. 任何一个Win32进程包含至少一个线程(越来越多, 后来就是多线程了), 并且如果我们运行其他的任务或者从我们的应用程序中打开其他的应用程序, 那么这些任务都会属于运行多线程集合的我们的那个Win32进程.
Win32进程有一个特点, 那就是它和虚拟边界很相似. 在进程内通信很容易, 但是想要在某种水平上与Win32进程外通信就要受到诸多限制. 为了与其他的Win32进程通信, 我们需要一些特别的工作机制, 因为有一些安全上下文需要考虑(security contexts ), 并且还需要考虑在特定系统中, Win32进程能做什么和不能做什么.
那么谁负责运行一个进程? 一个进程成功地运行都要涉及到哪些因素呢? 进程的执行以及进程中我们的代码的运行都是在: 域(domain)和操作系统的特权下的. 在维护一个活动状态的进程时, 操作系统不得不处理许多复杂的情况和要点.
让我们来看一个实际的情形吧:
考虑一个如下的情形, 我们的一个服务器内寄宿着很多个web应用程序, 或者说, 我们可能不得不在我们系统中运行一堆Windows应用程序. 现在, 瞧瞧我们正在处理什么, 瞧瞧我们得给这些应用程序什么吧- 资源, 资源, 甚至更多的资源! 我们的资源(内存)是非常昂贵的(这让我想起了当年玩经典的id software公司的游戏毁灭战士的情形, 在4兆内存的66兆赫兹的486PC上, 我总是不得不禁用MS-Dos 6.2的smartdrv.com这个程序, 那时内存可是纯粹的奢侈品). 并且在我们为不同客户同时运行程序的时候, 安全问题开始出现了. 所以迫切需要我们彻底禁止任何方式的这些应用程序的进程间的通信(然而, 这总是和需求有冲突.). 这样做会导致经常性的崩溃(天哪, 对这样的Windows都习以为常了!) 经常一个进程用光了被另一个进程申请来的内存, 然后就导致崩溃! 我恨崩溃! 人人都恨崩溃!(当然, 通过不断推出的新版本来维持他们的市场的微软除外). 不管怎样, 我们的进程很糟糕. 没啥新鲜的, 这些频繁的崩溃和运行时错误通常是由低效率的内存使用所引发的内存泄露, 对象空引用, 内存越界等等造成的. 所以, 大家都越来越意识到, 在一个多用户的环境下创建, 运行和维护进程是非常非常昂贵的. 因此, 运行一大堆进程并不是个好主意, 因为他们递增适应性不怎么好.
在这种情况下, 一种非常精巧的解决方案应运而生. 在同一个宿主进程下运行多个应用程序可以让我们使用更少的资源. 这甚至会导致更快的执行速度. 但是有另一个每天都发生的场景: 一旦一个应用程序崩溃, 所有其他的在这个相同进程中的应用程序就会像一副纸牌一样全部玩儿完!
是的, 我们说的就是这样的多米诺骨牌效应.
那现在怎么办呢?
使用.NET AppDomain吧. 这个概念非常巧妙, 有新意, 配得上诺贝尔奖. 引入Application domain的主要目的, 就是将我们的应用程序与其他的应用程序隔离开. Application domains运行在一个单独的Win32进程里. 与我们刚刚谈到的解决方案的相同, 通过在application domain内运行我们的应用程序, 来限制由内存泄露引起的错误和崩溃.
因此, 我们在一个应用程序域内运行应用程序, 并且我们在单个Win32进程中同时运行多个应用程序域. 借着CLR的运行托管代码的能力, 我们可以进一步的减少泄露和崩溃(还要感谢CLR的垃圾收集器). 在同一个应用程序域内的对象之间直接通信, 而存活在不同的应用程序域中的对象, 如果想要彼此通信的话, 就要通过互相拷贝对象或者通过用于消息互换的代理了(通过引用).
这就是Application Domain的关键之处. 但是还有更多. Application Domain相当于在单Win32进程内运行的一个精巧的轻量级进程. 事实上, 如同我们上面所说, 我们可以在一个Win32进程中运行多个Application Domain. 另一个AppDomain的优势是, 我们可以通过宿主(比如说ASP.NET)来干掉AppDomain, 而不会影响到其他的正存在于那个进程中的AppDomain. 所以, 我们在Application domain中的工作是独立的. 更进一步地, 我们可以通过销毁AppDomain来卸载掉加载到那个AppDomain中的对象. 神奇的.NET运行时通过接管对内存的控制来强制使用AppDomain分隔机制, 所以Win32进程中的AppDomain里使用的所有内存都由.NET运行时管理. 这样我们就避免了刚开头时提及的, 所有的, 诸如一个应用程序访问另一个应用程序的内存, 之类的问题, 也就避免了由崩溃引起的运行时错误问题. 因此我们实际上应用了一个安全层, 隔离了当前的应用程序, 与其他的应用程序分开. 实际上讲, 在我们创建类似与运行着的web services一样的应用程序时, Application Domain在其中扮演着一个安全方面和基础方面的关键角色.
所以说我们的AppDomain应该得诺贝尔奖, .NET framework更是如此. 现在我们来看看如何创建一个基本的应用程序域.
小菜一碟, 实际上. .NET Framework提供了一个漂亮的基类, 它存在于System命名空间下, 通过它我们可以显式地创建一个AppDomain. 在我们的应用程序中, 继承System.MarshalByRefObject基类, 我们可以创建可以在不同应用程序域间通信的对象.
看看这个吧, 一个使用C#创建AppDomain的HelloWorld应用程序. 我们使用的是Windows 控制台程序.
【译注:下面的代码无法直接在Visual Studio中运行,有错误。MSDN上有一个差不多的,但是说明性更强的代码,贴在本文的最后,供您参考。】
using System;
using System.Reflection;
using System.Runtime.Remoting;
public class ShowAppDomain : MarshalByRefObject
{
public string GetAppDomainName()
{
return AppDomain.CurrentDomain.FriendlyName; // gets the Current application domain thread
//and returns it's nameAppDomain name
}
}
public class CallMyHelloWorld
{
public static void Main()
{
AppDomain ad = AppDomain.CreateDomain("Yupee! My AppDomain!"); // create a new domain
ShowAppDomain ad2 = (ShowAppDomain)ad.CreateInstanceAndUnwrap(
Assembly.GetCallingAssembly().GetName().Name, "ShowAppDomain");
/*
We use the AppDomain.CreateInstanceAndUnwrap method to Create a new instance of a specified type.
Assembly.GetCallingAssembly().GetName().Name returns The assembly object and
*the name of the method that calls the presently executing method.
*/
Console.WriteLine("Here's my own AppDomain " + ad2.GetAppDomainName()); // Voila!
}
}
一部分AppDomain的使用方式涉及到使用Web Services的remoting. 事实上即使我们自己的.ASP.NET应用程序也是在应用程序域中创建的, 并且存在于工作者进程中(w3wp.exe).
到现在一直都还不错. 这就好像是开始吃一个麦香堡组合, 加法式炸薯条, 香嫩多汁的内在, 完美的咸脆的外表, 还有脑子里想着这顿饭其他的东西更加美味可口!
【译者注:MSDN上的关于介绍AppDomain的代码,可以贴到Visual Studio的控制台程序里跑跑看。】
using System;
using System.Reflection;
using System.Threading;
class Module1
{
public static void Main()
{
// Get and display the friendly name of the default AppDomain.
string callingDomainName = Thread.GetDomain().FriendlyName;
Console.WriteLine(callingDomainName);
// Get and display the full name of the EXE assembly.
string exeAssembly = Assembly.GetEntryAssembly().FullName;
Console.WriteLine(exeAssembly);
// Construct and initialize settings for a second AppDomain.
AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase =
System.Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile =
AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
// Create the second AppDomain.
AppDomain ad2 = AppDomain.CreateDomain("AD #2", null, ads);
// Create an instance of MarshalbyRefType in the second AppDomain.
// A proxy to the object is returned.
MarshalByRefType mbrt =
(MarshalByRefType)ad2.CreateInstanceAndUnwrap(
exeAssembly,
typeof(MarshalByRefType).FullName
);
// Call a method on the object via the proxy, passing the
// default AppDomain's friendly name in as a parameter.
mbrt.SomeMethod(callingDomainName);
// Unload the second AppDomain. This deletes its object and
// invalidates the proxy object.
AppDomain.Unload(ad2);
try
{
// Call the method again. Note that this time it fails
// because the second AppDomain was unloaded.
mbrt.SomeMethod(callingDomainName);
Console.WriteLine("Sucessful call.");
}
catch (AppDomainUnloadedException)
{
Console.WriteLine("Failed call; this is expected.");
}
}
}
// Because this class is derived from MarshalByRefObject, a proxy
// to a MarshalByRefType object can be returned across an AppDomain
// boundary.
public class MarshalByRefType : MarshalByRefObject
{
// Call this method via a proxy.
public void SomeMethod(string callingDomainName)
{
// Get this AppDomain's settings and display some of them.
AppDomainSetup ads = AppDomain.CurrentDomain.SetupInformation;
Console.WriteLine("AppName={0}, AppBase={1}, ConfigFile={2}",
ads.ApplicationName,
ads.ApplicationBase,
ads.ConfigurationFile
);
// Display the name of the calling AppDomain and the name
// of the second domain.
// NOTE: The application's thread has transitioned between
// AppDomains.
Console.WriteLine("Calling from '{0}' to '{1}'.",
callingDomainName,
Thread.GetDomain().FriendlyName
);
}
}
/* This code produces output similar to the following:
AppDomainX.exe
AppDomainX, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
AppName=, AppBase=C:\AppDomain\bin, ConfigFile=C:\AppDomain\bin\AppDomainX.exe.config
Calling from 'AppDomainX.exe' to 'AD #2'.
Failed call; this is expected.
*/
翻译后记: 一看笔法就知道是个老美, 言辞随意且口语化, 颇有嘻哈随意的感觉, 这种笔法写技术文章, 并不多见, 呵呵.