C#:进程线程应用程序域(AppDomain)与上下文分析
Posted 烟雨楼台^浮云往事
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#:进程线程应用程序域(AppDomain)与上下文分析相关的知识,希望对你有一定的参考价值。
进程
进程是操作系统用于隔离众多正在运行的应用程序的机制。在.Net之前,每一个应用程序被加载到单独的进程中,并为该进程指定私有的虚拟内存。进程不能直接访问物理内存,操作系统通过其它的处理把这些虚拟内存映射到物理内存或IO设备的某个区域,而这些物理内存之间不会有重叠,这就决定了一个进程不可能访问分配给另一个进程的内存。相应地,运行在该进程中的应用程序也不可能写入另一个应用程序的内存,这确保了任何执行出错的代码不会损害其地址空间以外的应用程序。在这种机制下,进程作为应用程序之间一个独立而安全的边界在很大程度上提高了运行安全。
进程的缺点是降低了性能。许多一起工作的进程需要相互通信,而进程却不能共享任何内存,你不能通过任何有意义的方式使用从一个进程传递到另一个进程的内存指针。此外,你不能在两个进程间进行直接调用。你必须代之以使用代理,它提供一定程度的间接性。虽然,使用动态连接库dll让所有的组件运行在同一空间,一定程度上可以提高性能,但这些组件相互影响,一个组件的错误将极有可能导致整个应用程序的崩溃,“dll地狱”更是让许多应用程序难以避免。
线程
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.
线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU.
应用程序域(AppDomain)
在.Net中,应用程序有了一个新的边界:应用程序域(以下简称域)。它是一个用于隔离应用程序的虚拟边界。为了禁止不应交互的代码进行交互,这种隔离是必要的。.Net的应用程序在域层次上进行隔离,一个域中的应用程序不能直接访问另一个域中的代码和数据。这种隔离使得在一个应用程序范围内创建的所有对象都在一个域内创建,确保在同一进程中一个域内运行的代码不会影响其他域内的应用程序,大大提高了运行的安全。
.Net结构中,由于公共语言运行库能够验证代码是否为类型安全的代码,所以它可以提供与进程边界一样大的隔离级别,其性能开销也要低得多。你可以在单个进程中运行几个域,而不会造成进程间调用或切换等方面的额外开销。这种方法是把任何一个进程分解到多个域中,允许多个应用程序在同一进程中运行,每个域大致对应一个应用程序,运行的每个线程都在一个特殊的域中。如果不同的可执行文件都运行在同一个进程空间中,它们就能轻松地共享数据或直接访问彼此的数据。这种代码同运行同一个进程但域不同的类型安全代码一起运行时是安全的。在一个进程内运行多个应用程序的能力显著增强了服务器的可伸缩性。
域与线程的关系
在.Net中,线程是公共语言运行库用来执行代码的操作系统构造。在运行时,所有托管代码均加载到一个域中,由特定的操作系统线程来运行。然而,域和线程之间并不具有一一对应关系。在任意给定时间,单个域中可以执行不止一个线程,而且特定线程也并不局限在单个域内。也就是说,线程可以跨越域边界,不为每个域创建新线程。当然,在指定时刻,每一线程都只能在一个域中执行。运行库会跟踪所有域中有哪些线程正在运行。通过调用.Net类库的 Thread.GetDomain 方法,你还可以确定正在执行的线程所在的域。
一、进程的概念与作用
二、应用程序域
三、深入了解.NET上下文
四、进程应用程序域与线程的关系
一、进程的概念与作用
进程(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
以上是关于C#:进程线程应用程序域(AppDomain)与上下文分析的主要内容,如果未能解决你的问题,请参考以下文章