代码改变世界

C# 线程手册 第一章 线程定义 .NET 和 C# 对线程的支持

2012-01-05 16:47  DanielWise  阅读(4196)  评论(7编辑  收藏  举报

由于.NET Framework 支持自由线程,所以自由线程在所有.NET 语言中都存在,包括C#和VB.NET. 在下一部分,我们将着重关注如何提供这种支持以及更多关于线程是如何做到的,而不再关注线程是什么。我们将讨论一些能够进一步帮助区分进程的额外支持。

在这一部分的最后,你将理解:

1. 什么是System.AppDomain 类以及它可以帮助你做什么?

2. .NET runtime(运行时)如何监控线程?

System.AppDomain

当我们在这一章的早些时候解释进程时,我们知道进程是对维系进程存在的内存和资源的物理隔离。我们后来说到一个进程至少有一个线程。当初微软设计.NET Framework 时,它又添加了一层称作应用程序域或AppDomain的隔离。应用程序域不是像进程那样的物理隔离;它是进程内部的进一步的逻辑隔离。由于在一个进程中可能有多个应用程序域,所以我们有一些优势。大体上说,对标准进程来说不通过代理访问其他进程的数据是不可能的, 而使用代理会导致重大开销和代码复杂化。然而,通过介绍应用程序域的概念,我们现在可以在一个进程中运行多个程序。进程提供的隔离在应用程序域中也存在。线程可以在不同应用程序域间执行而没有与相关的内部进程通信开销。这些额外的进程内部的壁垒的好处是他们对内部数据提供类型检查。

微软将这些应用程序域相关的所有功能封装到一个System.AppDomain的类中。微软.NET 程序集与这些应用程序域之间有紧密联系。任何时候当一个程序集被加载到一个程序中时,它实际上是被加载到应用程序域中。除非特别情况,程序集都会被加载到调用代码的应用程序域中。应用程序域与线程也有一个直接关系;它们可以有一个或多个线程,就像进程一样。然而,不同点是一个应用程序域可能在进程内部创建而不是通过新的线程创建。这个关系可以简化成如图9所示的模型。

图9

 

在.NET 中,AppDomain和线程类由于安全原因而不能继承。

每个应用程序都包含一个或者多个AppDomains.每个AppDomain可以创建并执行多个线程。下图显示在机器X上有两个操作系统进程Y和Z。操作系统进程Y有四个应用程序域:A,B,C和D。操作系统进程Z有两个应用程序域:A和B。

图10

 

设置AppDomain数据

你已经听了理论看了模型;现在我们要动手写点真正的代码。在下面的例子中,我们将使用AppDomain设置数据,收集数据并确定AppDomain中正在运行的线程。创建一个新的类文件appdomain.cs并输入以下代码:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MyAppDomain
{
class MyAppDomain
{
private AppDomain mDomain;
private int mThreadId;

public void SetDomainData(string vName, string vValue)
{
mDomain.SetData(vName, (object)vValue);
//mThreadId = AppDomain.GetCurrentThreadId();
mThreadId = Thread.CurrentThread.ManagedThreadId;
}

public string GetDomainData(string name)
{
return (string)mDomain.GetData(name);
}

static void Main(string[] args)
{
string dataName = "MyData";
string dataValue = "Some Data to be stored";

Console.WriteLine("Retrieving current domain");
MyAppDomain obj = new MyAppDomain();
obj.mDomain = AppDomain.CurrentDomain;

Console.WriteLine("Setting domain data");
obj.SetDomainData(dataName, dataValue);

Console.WriteLine("Getting domain data");
Console.WriteLine("The data found for key " + dataName
+ " is " + obj.GetDomainData(dataName)
+ " running on thread id: " + obj.mThreadId);
}
}
}

你的输出应该类似于:

这即使对于非C#开发人员来说也很直观。然而,让我们看一下代码并确定究竟发生了什么。这是这个类的第一个重要地方:

public void SetDomainData(string vName, string vValue)
{
mDomain.SetData(vName, (object)vValue);
//mThreadId = AppDomain.GetCurrentThreadId();
mThreadId = Thread.CurrentThread.ManagedThreadId;
}

这个方法把设置名字和值的数据作为参数。你将会注意到SetData() 方法当传递参数时已经做了一些不同的事情。这里我们将字符串转换成一个Object类型,因为SetData()的第二个参数类型是object. 由于我们仅使用一个字符串和一个继承自System.Object的字符串,我们可以直接使用这个变量而不用把它强制转换为一个对象。然而,其他的你想要存储的数据可能不像现在这个容易处理。这个事实简单地提醒我们已经完成了这个转换。在这个方法的最后部分,你将注意到我们可以通过对AppDomain对象的GetCurrentThreadId属性的简单调用获取当前执行线程的ID(已经过时,新方法是 Thread.CurrentThread.ManagedThreadId)。

让我们继续下一个方法:

public string GetDomainData(string name)
{
return (string)mDomain.GetData(name);
}

这个方法也很基础。我们使用AppDomain类的GetData()方法获取一个基于键值的数据。在这种情况下,我们仅是将GetDomainData()方法的参数传递给GetData()方法。我们将GetData方法的结果返回。

最后,让我们看一下主方法:

static void Main(string[] args)
{
string dataName = "MyData";
string dataValue = "Some Data to be stored";

Console.WriteLine("Retrieving current domain");
MyAppDomain obj = new MyAppDomain();
obj.mDomain = AppDomain.CurrentDomain;

Console.WriteLine("Setting domain data");
obj.SetDomainData(dataName, dataValue);

Console.WriteLine("Getting domain data");
Console.WriteLine("The data found for key "
+ dataName
+ " is " + obj.GetDomainData(dataName)
+ " running on thread id: " + obj.mThreadId);
}

我们通过初始化我们想在AppDomain中存储的名值对开始并向控制台写一段代码提示我们方法已经开始执行。下一步,我们使用对当前执行的AppDomain对象(Main()方法中执行的那个对象)的引用对我们类中的Domain字段赋值。下一步我们调用方法-传递参数给SetDomainData()方法:

obj.SetDomainData(dataName, dataValue);

继续,我们向GetDomainData()方法传递一个参数来获取我们刚设置的数据并把它插入到控制台输出流中。我们也输出我们的类的ThreadId属性来看当前调用方法的ThreadId.

在一个特定AppDomain中执行代码

现在让我们看一下如何创建一个新的应用程序域并观察当在新创建的AppDomain中创建线程时的重要行为。下面代码包含在create_appdomain.cs中:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace MyAppDomain
{
public class CreateAppDomains
{
static void Main(string[] args)
{
AppDomain domainA = AppDomain.CreateDomain("MyDomainA");
string stringA = "DomainA Value";
domainA.SetData("DomainKey", stringA);

CommonCallBack();

CrossAppDomainDelegate delegateA =
new CrossAppDomainDelegate(CommonCallBack);
domainA.DoCallBack(delegateA);
}

static void CommonCallBack()
{
AppDomain domain = AppDomain.CurrentDomain;
Console.WriteLine("The value '" + domain.GetData("DomainKey")
+ "' was found in " + domain.FriendlyName
+ " running on thread id: "
+ Thread.CurrentThread.ManagedThreadId);
}
}
}

编译类的输出看起来应该像这样:

你会注意到我们在这个例子中创建了两个应用程序域。我们调用了AppDomain类中的静态CreateDomain()方法。构造函数参数是我们创建的AppDomain的一个友好名字。稍后我们将看到可以通过一个只读属性访问这个友好名字。下面是创建AppDomain实例的代码:

AppDomain domainA = AppDomain.CreateDomain("MyDomainA");

下一步我们调用先前例子中的SetData()方法。由于之前已经介绍过这个方法所以这里就略过。然而,我们需要解释的是如何在一个给定的AppDomain中获得代码运行。通过AppDomain类中的DoCallBack()方法可以实现。这个方法将一个CrossAppDomainDelegate作为它的参数。在这种情况下,我们已经创建了一个CrossAppDomainDelegate的实例并将它的名字作为参数传递给我们希望执行的构造函数中去。

CommonCallBack();

CrossAppDomainDelegate delegateA = new CrossAppDomainDelegate(CommonCallBack);
domainA.DoCallBack(delegateA);

我们首先调用CommonCallBack()。这是要在主AppDomain的上下文中执行CommonCallBack() 方法。你将看到输出的是主AppDomain的FriendlyName属性是执行者名字。

最后,看一下CommonCallBack()方法本身:

static void CommonCallBack()
{
AppDomain domain = AppDomain.CurrentDomain;
Console.WriteLine("The value '" + domain.GetData("DomainKey")
+ "' was found in " + domain.FriendlyName
+ " running on thread id: "
+ Thread.CurrentThread.ManagedThreadId);
}

你会发现它非常原子化以至于不论在什么实例下运行都会工作。我们再次使用CurrentDomain属性获取执行代码的应用程序域的一个引用。然后我们再次使用FriendlyName属性确定我们在使用哪个AppDomain.

我们也调用了GetCurrentThreadId()方法(已过时,同上)。当你查看输出,你将看到不论我们在哪个AppDomain中执行都会得到同样的线程ID。需要知道不论一个AppDomain有没有线程,线程都可以跨应用程序域执行,这是很重要的。

线程管理和.NET运行时

.NET Framework 提供比进程自由线程和逻辑应用程序域还有多的特性。事实上,.NET Framework 提供对处理器线程的对象表示。这些对象表示是System.Threading.Thread类的实例。我们将在下一章深入探讨。然而,再继续下一章之前,我们必须了解非托管线程托管线程是如何关联的。那就是说,非托管线程(在.NET 世界之外创建的线程)实例有关,后者表示运行在.NET CLR 中的线程。

.NET 运行时监控所有由.NET代码创建的线程。它也监控所有可能在托管代码中执行的非托管线程。由于托管代码可以通过COM-可调用包装暴露,所以非托管线程运行在.NET运行时中是可能的。

当非托管代码运行在一个托管线程中,运行时将会检查一个托管线程对象的TLS是否存在。如果找到了一个托管线程,运行时就会使用它。否则它将创建一个新的然后使用。这很简单,但是需要注意。我们仍想要得到一个关于我们线程的对象表示而不管它来自哪里。如果运行时无法管理且为外部调用类型创建线程,我们将无法在托管环境中确定线程,甚至控制它。

关于线程管理最后一件重要的事是一旦非托管调用返回到非托管代码中,运行时将无法继续检测它。

总结

我们在这一章讲了很多内容。关于什么是多任务以及如何通过使用线程实现多任务。知道了多任务和自由线程不是一回事儿。还讲了进程以及如何与其他应用程序隔离。我们也讲述了Windows操作系统中线程方法。你现在知道Windows会将当前线程中断以使其他线程获取一个简单的周期作为运行时间。这个简单的周期称作一个时间片或间歇。我们也描述了线程优先级功能和这些优先级的不同级别,以及线程默认情况下会继承父进程的优先级。

我们也描述了.NET 运行时如何监控在.NET环境中创建的线程以及在托管代码中执行的非托管线程。还描述了.NET Framework对线程的支持。System.AppDomain类在进程物理数据隔离的基础上提供额外层的逻辑数据隔离。我们描述了线程如何轻松地从一个AppDomain到另外一个AppDomain. 还有我们也查看了为何一个AppDomain没有像进程一样有自己的线程。