C#综合揭秘——细说进程、应用程序域与上下文之间的关系
引言
本文主要是介绍进程(Process)、应用程序域(AppDomain)、.NET上下文(Context)的概念与操作。
虽然在一般的开发当中这三者并不常用,但熟悉三者的关系,深入了解其作用,对提高系统的性能有莫大的帮助。
在本篇最后的一节当中将会介绍到三者与线程之间的关系,希望对多线程开发人员能提供一定的帮助。
因为时间仓促,文中有错误的地方敬请点评。
目录
一、进程的概念与作用
进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
1.1 Process 的属性与方法
在 System.Diagnostics 命名空间当中存在Process类,专门用于管理进程的开始、结束,访问进程中的模块,获取进程中的线程,设定进程的优先级别等。
表1.0 显示了Process类的常用属性:
属性 | 说明 |
BasePriority | 获取关联进程的基本优先级。 |
ExitCode | 获取关联进程终止时指定的值。 |
ExitTime | 获取关联进程退出的时间。 |
Handle | 返回关联进程的本机句柄。 |
HandleCount | 获取由进程打开的句柄数。 |
HasExited | 获取指示关联进程是否已终止的值。 |
Id | 获取关联进程的唯一标识符。 |
MachineName | 获取关联进程正在其上运行的计算机的名称。 |
MainModule | 获取关联进程的主模块。 |
Modules | 获取已由关联进程加载的模块。 |
PriorityClass | 获取或设置关联进程的总体优先级类别。 |
ProcessName | 获取该进程的名称。 |
StartInfo | 获取或设置要传递给Process的Start方法的属性。 |
StartTime | 获取关联进程启动的时间。 |
SynchronizingObject | 获取或设置用于封送由于进程退出事件而发出的事件处理程序调用的对象。 |
Threads | 获取在关联进程中运行的一组线程 |
表1.0
除了上述属性,Process类也定义了下列经常使用的方法:
方法 | 说明 |
GetProcessById | 创建新的 Process 组件,并将其与您指定的现有进程资源关联。 |
GetProcessByName | 创建多个新的 Process 组件,并将其与您指定的现有进程资源关联。 |
GetCurrentProcess | 获取新的 Process 组件并将其与当前活动的进程关联。 |
GetProcesses | 获取本地计算机上正在运行的每一个进程列表。 |
Start | 启动一个进程。 |
Kill | 立即停止关联的进程。 |
Close | 释放与此组件关联的所有资源。 |
WaitForExit | 指示 Process 组件无限期地等待关联进程退出。 |
表1.1
Process类的详细信息可以参考 http://msdn.microsoft.com/zh-cn/library/system.diagnostics.process.aspx
下面将举例介绍一下Process的使用方式
1.2 建立与销毁进程
利用 Start 与Kill 方法可以简单建立或者销毁进程,下面例子就是利用 Start 方法启动记事本的进程,并打开File.txt文件。2秒钟以后,再使用 Kill 方法销毁进程,并关闭记事本。
1 static void Main(string[] args)
2 {
3 Process process = Process.Start("notepad.exe","File.txt");
4 Thread.Sleep(2000);
5 process.Kill();
6 }
1.3 列举计算机运行中的进程
在表1.0 中可以看到,使用 GetProcesses 方法可以获取本地计算机上正在运行的每一个进程列表。
而进程的 Id 属性是每个进程的唯一标志,通过下面的方法,可以显示当前计算机运行的所有进程信息。
因为篇幅关系,下面例子只获取前10个进程。
1 static void Main(string[] args)
2 {
3 var processList = Process.GetProcesses()
4 .OrderBy(x=>x.Id)
5 .Take(10);
6 foreach (var process in processList)
7 Console.WriteLine(string.Format("ProcessId is:{0} \t ProcessName is:{1}",
8 process.Id, process.ProcessName));
9 Console.ReadKey();
10 }
运行结果
如果已知进程的Id,就可以通过 GetProcessById 方法获取对应的进程。
1 static void Main(string[] args)
2 {
3 try
4 {
5 var process = Process.GetProcessById(1772);
6 Console.WriteLine("Process name is:" + process.ProcessName);
7 }
8 catch (ArgumentException ex)
9 {
10 Console.WriteLine("Process is nothing!");
11 }
12 Console.ReadKey();
13 }
同样地,你也可能通过GetProcessByName方法获取多个对应名称的进程。
注意:如果不能找到当前ID的进程,系统就会抛出ArgumentException异常。所以使用方法 GetProcessById 获取进程时应该包含在 try{...} catch{..} 之内。
1.4 获取进程中的多个模块
在表1.0 中包含了Process类的Modules属性,通过此属性可能获取进程中的多个模块。
这些模块可以是以 *.dll 结尾的程序集,也可以是 *.exe 结尾的可执行程序。
下面的例子就是通过 Process 的 GetCurrentProcess 方法获取当前运行的进程信息,然后显示当前进程的多个模块信息。
1 static void Main(string[] args)
2 {
3 var moduleList = Process.GetCurrentProcess().Modules;
4 foreach (System.Diagnostics.ProcessModule module in moduleList)
5 Console.WriteLine(string.Format("{0}\n URL:{1}\n Version:{2}",
6 module.ModuleName,module.FileName,module.FileVersionInfo.FileVersion));
7 Console.ReadKey();
8 }
运行结果:
二、应用程序域
使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。应用程序域是.NET引入的一个新概念,它比进程所占用的资源要少,可以被看作是一个轻量级的进程。
在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll)。这样可以使应用程序域之间实现深度隔离,即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。
当一个程序集同时被多个应用程序域调用时,会出现两种情况:
第一种情况:CLR分别为不同的应用程序域加载此程序集。
第二种情况:CLR把此程序集加载到所有的应用程序域之外,并实现程序集共享,此情况比较特殊,被称作为Domain Neutral。
2.1 AppDomain的属性与方法
在System命名空间当中就存在AppDomain类,用管理应用程序域。下面是AppDomain类的常用属性:
属性 | 说明 |
ActivationContext | 获取当前应用程序域的激活上下文。 |
ApplicationIdentity | 获得应用程序域中的应用程序标识。 |
BaseDirectory | 获取基目录。 |
CurrentDomain | 获取当前 Thread 的当前应用程序域。 |
Id | 获得一个整数,该整数唯一标识进程中的应用程序域。 |
RelativeSearchPath | 获取相对于基目录的路径,在此程序集冲突解决程序应探测专用程序集。 |
SetupInformation | 获取此实例的应用程序域配置信息。 |
表2.0
AppDomain类中有多个方法,可以用于创建一个新的应用程序域,或者执行应用程序域中的应用程序。
方法 | 说明 |
CreateDomain | 创建新的应用程序域。 |
CreateInstance | 创建在指定程序集中定义的指定类型的新实例。 |
CreateInstanceFrom | 创建在指定程序集文件中定义的指定类型的新实例。 |
DoCallBack | 在另一个应用程序域中执行代码,该应用程序域由指定的委托标识。 |
ExecuteAssembly | 执行指定文件中包含的程序集。 |
ExecuteAssemblyByName | 执行程序集。 |
GetAssemblies | 获取已加载到此应用程序域的执行上下文中的程序集。 |
GetCurrentThreadId | 获取当前线程标识符。 |
GetData | 为指定名称获取存储在当前应用程序域中的值。 |
IsDefaultAppDomain | 返回一个值,指示应用程序域是否是进程的默认应用程序域。 |
SetData | 为应用程序域属性分配值。 |
Load | 将 Assembly 加载到此应用程序域中。 |
Unload | 卸载指定的应用程序域。 |
表2.1
AppDomain类中有多个事件,用于管理应用程序域生命周期中的不同部分。
事件 | 说明 |
AssemblyLoad | 在加载程序集时发生。 |
AssemblyResolve | 在对程序集的解析失败时发生。 |
DomainUnload | 在即将卸载 AppDomain 时发生。 |
ProcessExit | 当默认应用程序域的父进程存在时发生。 |
ReflectionOnlyAssemblyResolve | 当程序集的解析在只反射上下文中失败时发生。 |
ResourceResolve | 当资源解析因资源不是程序集中的有效链接资源或嵌入资源而失败时发生。 |
TypeResolve | 在对类型的解析失败时发生。 |
UnhandledException | 当某个异常未被捕获时出现。 |
表2.2
下面将举例详细介绍一下AppDomain的使用方式
2.2 在AppDomain中加载程序集
由表2.1中可以看到,通过CreateDomain方法可以建立一个新的应用程序域。
下面的例子将使用CreateDomain建立一个应用程序域,并使用Load方法加载程序集Model.dll。最后使用GetAssemblies方法,列举此应用程序域中的所有程序集。
1 static void Main(string[] args)
2 {
3 var appDomain = AppDomain.CreateDomain("NewAppDomain");
4 appDomain.Load("Model");
5 foreach (var assembly in appDomain.GetAssemblies())
6 Console.WriteLine(string.Format("{0}\n----------------------------",
7 assembly.FullName));
8 Console.ReadKey();
9 }
运行结果
注意:当加载程序集后,就无法把它从AppDomain中卸载,只能把整个AppDomain卸载。
当需要在AppDomain加载可执行程序时,可以使用ExecuteAssembly方法。
AppDomain.ExecuteAssembly("Example.exe");
2.3 卸载AppDomain
通过Unload可以卸载AppDomain,在AppDomain卸载时将会触发DomainUnload事件。
下面的例子中,将会使用CreateDomain建立一个名为NewAppDomain的应用程序域。然后建立AssemblyLoad的事件处理方法,在程序集加载时显示程序集的信息。最后建立DomainUnload事件处理方法,在AppDomain卸载时显示卸载信息。
1 static void Main(string[] args)
2 {
3 //新建名为NewAppDomain的应用程序域
4 AppDomain newAppDomain = AppDomain.CreateDomain("NewAppDomain");
5 //建立AssemblyLoad事件处理方法
6 newAppDomain.AssemblyLoad +=
7 (obj, e) =>
8 {
9 Console.WriteLine(string.Format("{0} is loading!", e.LoadedAssembly.GetName()));
10 };
11 //建立DomainUnload事件处理方法
12 newAppDomain.DomainUnload +=
13 (obj, e) =>
14 {
15 Console.WriteLine("NewAppDomain Unload!");
16 };
17 //加载程序集
18 newAppDomain.Load("Model");
19 //模拟操作
20 for (int n = 0; n < 5; n++)
21 Console.WriteLine(" Do Work.......!");
22 //卸载AppDomain
23 AppDomain.Unload(newAppDomain);
24 Console.ReadKey();
25 }
运行结果
2.4 在AppDomain中建立程序集中指定类的对象
使用CreateInstance方法,能建立程序集中指定类的对像。但使用此方法将返回一个ObjectHandle对象,若要将此值转化为原类型,可调用Unwrap方法。
下面例子会建立Model.dll程序集中的Model.Person对象。
1 namespace Test
2 {
3 public class Program
4 {
5 static void Main(string[] args)
6 {
7 var person=(Person)AppDomain.CurrentDomain
8 .CreateInstance("Model","Model.Person").Unwrap();
9 person.ID = 1;
10 person.Name = "Leslie";
11 person.Age = 29;
12 Console.WriteLine(string.Format("{0}'s age is {1}!",person.Name,person.Age));
13 Console.ReadKey();
14 }
15 }
16 }
17
18 namespace Model
19 {
20 public class Person
21 {
22 public int ID
23 {
24 get;
25 set;
26 }
27 public string Name
28 {
29 get;
30 set;
31 }
32 public int Age
33 {
34 get;
35 set;
36 }
37 }
38 }
三、深入了解.NET上下文
3.1 .NET上下文的概念
应用程序域是进程中承载程序集的逻辑分区,在应用程序域当中,存在更细粒度的用于承载.NET对象的实体,那就.NET上下文Context。
所有的.NET对象都存在于上下文当中,每个AppDomain当中至少存在一个默认上下文(context 0)。
一般不需要指定特定上下文的对象被称为上下文灵活对象(context-agile),建立此对象不需要特定的操作,只需要由CLR自行管理,一般这些对象都会被建立在默认上下文当中。
图3.0
3.2 透明代理
在上下文的接口当中存在着一个消息接收器负责检测拦截和处理信息,当对象是MarshalByRefObject的子类的时候,CLR将会建立透明代理,实现对象与消息之间的转换。
应用程序域是CLR中资源的边界,一般情况下,应用程序域中的对象不能被外界的对象所访问。而MarshalByRefObject 的功能就是允许在支持远程处理的应用程序中跨应用程序域边界访问对象,在使用.NET Remoting远程对象开发时经常使用到的一个父类。
此文章针对的是进程与应用程序域的作用,关于MarshalByRefObject的使用已经超越了本文的范围,关于.NET Remoting 远程对象开发可参考:“回顾.NET Remoting分布式开发”。
3.3 上下文绑定
当系统需要对象使用消息接收器机制的时候,即可使用ContextBoundObject类。ContextBoundObject继承了MarshalByRefObject类,保证了它的子类都会通过透明代理被访问。
在第一节介绍过:一般类所建立的对象为上下文灵活对象(context-agile),它们都由CLR自动管理,可存在于任意的上下文当中。而 ContextBoundObject 的子类所建立的对象只能在建立它的对应上下文中正常运行,此状态被称为上下文绑定。其他对象想要访问ContextBoundObject 的子类对象时,都只能通过代透明理来操作。
下面的例子,是上下文绑定对象与上下文灵活对象的一个对比。Example 是一个普通类,它的对象会运行在默认上下文当中。而ContextBound类继承了ContextBoundObject,它的对象是一个上下文绑定对象。ContextBound还有一个Synchronization特性,此特性会保证ContextBound对象被加载到一个线程安全的上下文当中运行。另外,Context类存在ContextProperties属性,通过此属性可以获取该上下文的已有信息。
1 class Program
2 {
3 public class Example
4 {
5 public void Test()
6 {
7 ContextMessage("Example Test\n");
8 }
9 //访问上下文绑定对象测试
10 public void Sync(ContextBound contextBound)
11 {
12 contextBound.Test("Example call on contextBound\n");
13 }
14 }
15
16 [Synchronization]
17 public class ContextBound:ContextBoundObject
18 {
19 public void Test(string message)
20 {
21 ContextMessage(message);
22 }
23 }
24
25 static void Main(string[] args)
26 {
27 Example example = new Example();
28 example.Test();
29 ContextBound contextBound = new ContextBound();
30 contextBound.Test("ContentBound Test\n");
31 example.Sync(contextBound);
32 Console.ReadKey();
33 }
34
35 //显示上下文信息
36 public static void ContextMessage(string data)
37 {
38 Context context = Thread.CurrentContext;
39 Console.WriteLine(string.Format("{0}ContextId is {1}", data, context.ContextID));
40 foreach (var prop in context.ContextProperties)
41 Console.WriteLine(prop.Name);
42 Console.WriteLine();
43 }
44 }
运行结果
由运行结果可以发现,example对象一般只会工作于默认上下文context 0 当中,而contextBound则会工作于线程安全的上下文 context 1当中。当example需要调用contextBound对象时,就会通过透明代理把消息直接传递到context 1中。
四、进程、应用程序域、线程的相互关系
4.1 跨AppDomain运行代码
在应用程序域之间的数据是相对独立的,当需要在其他AppDomain当中执行当前AppDomain中的程序集代码时,可以使用CrossAppDomainDelegate委托。把CrossAppDomainDelegate委托绑定方法以后,通过AppDomain的DoCallBack方法即可执行委托。
1 static void Main(string[] args)
2 {
3 Console.WriteLine("CurrentAppDomain start!");
4 //建立新的应用程序域对象
5 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain");
6 //绑定CrossAppDomainDelegate的委托方法
7 CrossAppDomainDelegate crossAppDomainDelegate=new CrossAppDomainDelegate(MyCallBack);
8 //绑定DomainUnload的事件处理方法
9 newAppDomain.DomainUnload += (obj, e) =>
10 {
11 Console.WriteLine("NewAppDomain unload!");
12 };
13 //调用委托
14 newAppDomain.DoCallBack(crossAppDomainDelegate);
15 AppDomain.Unload(newAppDomain) ;
16 Console.ReadKey();
17 }
18
19 static public void MyCallBack()
20 {
21 string name = AppDomain.CurrentDomain.FriendlyName;
22 for(int n=0;n<4;n++)
23 Console.WriteLine(string.Format( " Do work in {0}........" , name));
24 }
运行结果
4.2 跨AppDomain的线程
线程存在于进程当中,它在不同的时刻可以运行于多个不同的AppDomain当中。它是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时 系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
关于线程的介绍,可参考 “C#综合揭秘——细说多线程(上)”、“C#综合揭秘——细说多线程(下)”
下面的例子将介绍一下如何跨AppDomain使用线程,首先建立一个ConsoleApplication项目,在执行时输入当前线程及应用程序域的信息,最后生成Example.exe的可执行程序。
1 static void Main(string[] args)
2 {
3 var message = string.Format(" CurrentThreadID is:{0}\tAppDomainID is:{1}",
4 Thread.CurrentThread.ManagedThreadId, AppDomain.CurrentDomain.Id);
5 Console.WriteLine(message);
6 Console.Read();
7 }
然后再新建一个ConsoleApplication项目,在此项目中新一个AppDomain对象,在新的AppDomain中通过ExecuteAssembly方法执行Example.exe程序。
1 static void Main(string[] args)
2 {
3 //当前应用程序域信息
4 Console.WriteLine("CurrentAppDomain start!");
5 ShowMessage();
6
7 //建立新的应用程序域对象
8 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain");
9 //在新的应用程序域中执行Example.exe
10 newAppDomain.ExecuteAssembly("Example.exe");
11
12 AppDomain.Unload(newAppDomain);
13 Console.ReadKey();
14 }
15
16 public static void ShowMessage()
17 {
18 var message = string.Format(" CurrentThreadID is:{0}\tAppDomainID is:{1}",
19 Thread.CurrentThread.ManagedThreadId, AppDomain.CurrentDomain.Id);
20 Console.WriteLine(message);
21 }
运行结果
可见,ID等于9的线程在不同时间内分别运行于AppDomain 1与AppDomain 2当中。
4.3 跨上下文的线程
线程既然能够跨越AppDomain的边界,当然也能跨越不同的上下文。
下面这个例子中,线程将同时运行在默认上下文与提供安全线程的上下文中。
1 class Program
2 {
3 [Synchronization]
4 public class ContextBound : ContextBoundObject
5 {
6 public void Test()
7 {
8 ShowMessage();
9 }
10 }
11
12 static void Main(string[] args)
13 {
14 //当前应用程序域信息
15 Console.WriteLine("CurrentAppDomain start!");
16 ShowMessage();
17
18 //在上下文绑定对象中运行线程
19 ContextBound contextBound = new ContextBound();
20 contextBound.Test();
21 Console.ReadKey();
22 }
23
24 public static void ShowMessage()
25 {
26 var message = string.Format(" CurrentThreadID is:{0}\tContextID is:{1}",
27 Thread.CurrentThread.ManagedThreadId, Thread.CurrentContext.ContextID);
28 Console.WriteLine(message);
29 }
30 }
运行结果
本篇总结
进程(Process)、线程(Thread)、应用程序域(AppDomain)、上下文(Context)的关系如图5.0,一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。线程也能穿梭于多个上下文当中,进行对象的调用。
虽然进程、应用程序域与上下文在平常的开发中并非经常用到,但深入地了解三者的关系,熟悉其操作方式对合理利用系统的资源,提高系统的效率是非常有意义的。
尤其是三者与线程之间的关系尤为重要,特别是在一个多线程系统中,如果不能理清其关系而盲目使用多线程,容易造成资源抢占与死锁之类的错误。
图5.0
希望本篇文章对相关的开发人员有所帮助。
对.NET开发有兴趣的朋友请加入博客园讨论小组“.NET高级编程”一起探讨!