(转)Scope<T> 及更多知识

 转自:http://www.microsoft.com/china/MSDN/library/netFramework/netframework/NETMattersSep.mspx?mfr=true

问 我一直在使用 .NET Framework 2.0 中的新 TransactionScope 类,我喜欢它所提供的模型。要启动一个事务,我可以在一个方法中使用 Transaction 创建一个 TransactionScope,然后在该方法所调用的另一方法中,可以创建一个自动列在该 Transaction 中的 SqlCommand。但 SqlCommand 如何知道先前创建的 Transaction?更重要的是,如何在我自己的类中模拟此功能?

答 如果您还不太熟悉 TransactionScope,那么告诉您,TransactionScope 是 Microsoft® .NET Framework 2.0 中新增的 System.Transactions 命名空间的一部分。System.Transactions 提供了完全集成到 .NET Framework 中的事务框架(包括但不局限于 ADO.NET)。Transaction 和 TransactionScope 是此命名空间中最重要的两个类。正如问题中所说,您可以创建一个 TransactionScope 实例,然后会在其中自动列出在该 TransactionScope 的作用域内执行的 ADO.NET 操作(您也可以通过 Transaction.Current 静态属性访问当前的 Transaction):

using(TransactionScope scope = new TransactionScope())
{
    ... // 此处的所有操作均为事务的一部分
    scope.Complete();}

TransactionScope 有多个构造函数,其中一些接受 TransactionScopeOption 枚举值,告诉 TransactionScope 是否创建新事务、是否使用任何可能已经存在的环境事务或者是否取消任何环境事务。如果我执行以下代码(这段代码将创建一个嵌套 TransactionScope,它需要新事务而不使用现有环境事务),我将看到首先输出原始 Transaction 的局部标识符,接着是新 Transaction 的标识符,然后又是原始标识符:

using (new TransactionScope())
{
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier);
    using (new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        Console.WriteLine(
            Transaction.Current.TransactionInformation.LocalIdentifier);
    }
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier); 
}

可以嵌套任意数量的 TransactionScope,在内部,System.Transaction 命名空间保留所有环境事务的堆栈。

在其核心,线程静态成员提供了以下模型。如果一个类型包含非静态字段(实例字段),则对于该字段,该类型的每个实例均有其自身的独立存储位置;在一个实例中设置字段并不影响其他实例中该字段的值。而相反,对于静态字段,无论有多少实例,该字段只位于一个存储位置(或者,更具体地说,在每个 AppDomain 中,只位于一个存储位置)。然而,如果将 System.ThreadStaticAttribute 应用于静态字段,则该字段将变为线程静态字段,即,对于该字段,每个线程(而非实例)将保留其自身的存储位置。在一个线程上设置线程静态的值将不会影响其在其他线程上的值。熟悉 Visual C++® 的人会知道,在概念上,此功能与 __declspec(thread) 中的该功能相似,用于在线程局部存储中声明线程局部变量。

线程静态字段可用于各种情况。一种常见用法是用于存储非线程安全类型的单例。通过为每个线程而不是整个 AppDomain 创建单例,并不需要进行显式锁定就可确保线程安全,因为只有一个线程可以访问变量,也就只有一个线程可以访问实例(当然,如果线程将对单例对象的引用交给另一线程,这种安全性将会丧失)。实际上,当使用静态单例时,通常需要两种锁定,一种用于初始化(如果初始化在类型的静态构造函数中完成,则属于隐式锁定,如果使用延迟初始化,则属于显示锁定),另一种用于访问实际实例。线程静态单例不需要这两种锁定。

线程静态字段的另外一种用法是(这又将我们带回到 TransactionScope)用于在方法调用之间传递带外数据。在非面向对象的语言中,通常情况下,函数有权访问的数据只能是通过参数或全局变量显示提供给它的数据。但是,在面向对象的系统中,还有许多其他方式,如,实例字段(如果方法为实例方法)或静态字段(如果方法为静态方法)。

您可以假设 TransactionScope 可以使用静态字段来存储当前的 Transaction,而 SqlCommand 只从该静态字段中获取相应 Transaction。然而,在多线程情况下,这可能会引发重大问题,因为两个 TransactionScope 可能会相互攻击。某一线程可能会覆盖另一线程最近发布的当前 Transaction 引用,这样,多个线程最终可能会使用同一 Transaction 实例,所有这些可能会引发一场灾难。

这回,线程静态字段有了用武之地。既然存储在线程静态字段中的数据只对存储该数据的同一线程中所运行的代码可见,那么,可使用此类字段将其他数据从一个方法传递到该第一个方法所调用的其他方法,而且完全不用担心其他线程会破坏它的工作。现在,假设 TransactionScope 使用了相似的技术。实例化后,它会将当前的 Transaction 存储到线程静态字段中。当稍后实例化 SqlCommand 时(在此 TransactionScope 从线程局部存储中删除之前),该 SqlCommand 会检查线程静态字段以查找现有 Transaction,如果存在则列入该 Transaction 中。通过这种方式,TransactionScope 和 SqlCommand 能够协同工作,从而开发人员不必将 Transaction 显示传递给 SqlCommand 对象。实际上,TransactionScope 和 SqlCommand 所使用的机制非常复杂,但核心是前提要合理。

可以使用线程静态字段创建任何类型的相似系统。图 1 显示了我编写的一个类:Scope<T>,它实现了有些相似的行为。理念是:您可以在一个方法中使用类型 T 的一个实例来实例化 Scope<T>,并希望调用堆栈中的另一方法稍后能够访问该实例。该方法可以使用 Scope<T>.Current 来访问实例。Scope<T> 也支持嵌套,即在内部,它保留类型 T 的实例堆栈,并通过 Current 属性将实例暴露在堆栈顶部。这样,如果一个方法创建了具有一个 T 类型实例的 Scope<T>,然后调用了用于实例化具有其他 T 类型实例的另一 Scope<T> 的其他方法,则第一个 Scope<T> 中的实例不会丢失,并可在删除嵌套作用域后,通过 Scope<T>.Current 再次进行访问。

应几乎始终严格限制环境属性(如 Scope<T>.Current)的作用域。换句话说,需在同一堆栈帧中发布和撤销这些属性(通常是通过 try/finally 块)。对于 TransactionScope、锁定、模拟等等也是如此。这也是 Scope<T> 实现 IDisposable 的原因:Scope<T> 将用于 using 块中,以便构造函数发布实例以及 Dispose 方法撤销该实例。

下面的示例应该可以帮助您巩固上面介绍的内容。图 2 显示了如何使用具有 StreamWriter 实例的 Scope<T> 的一个示例。首先,Main 方法将“MainEnter”写到由 Scope<StreamWriter>.Current 返回的 StreamWriter 中。因为当前没有处于活动状态的 Scope<StreamWriter>,Scope<StreamWriter>.Current 将返回空(这就是我使用了一个助手 Write 方法的原因,该方法用于在向 StreamWriter 中写入之前检查其是否为空,否则必定会生成 NullReferenceException)。然后,针对文件 C:\test1.txt,使用 StreamWriter 实例化新的 Scope<StreamWriter>,并调用 FirstMethod 方法。FirstMethod 将“FirstMethodEnter”写到当前 StreamWriter 中,同时,该文本将输出到 C:\test1.txt 中,因为它是当前作用域中的 StreamWriter。FirstMethod 继续设置一个新的 Scope<StreamWriter>,这次是针对 C:\test2.txt。在内部,对于 Scope<T>,这将导致新 StreamWriter 被压入内部堆栈,位于 C:\test1.txt 的现有 StreamWriter 之上。此时,Scope<StreamWriter>.Current 将返回对 C:\test2.txt 的 StreamWriter 的引用,因此,对 SecondMethod 的调用将导致“SecondMethod”被写入 C:\test2.txt,而不是 C:\test1.txt。FirstMethod 继续删除 C:\test2.txt 的 Scope<StreamWriter>(借助于 using 关键字),将该 StreamWriter 从内部堆栈中弹出,使 C:\test1.txt 的 StreamWriter 恢复为当前 StreamWriter,这样,在将“FirstMethodExit”写入 Scope<StreamWriter>.Current 时会将该文本指向 C:\test1.txt。最后,返回到 Main 中,原始 Scope<StreamWriter> 被删除,StreamWriter 的内部堆栈清空,这样,Scope<StreamWriter>.Current 再次返回空。好了,这下放心了。

在内部,Scope<T> 依赖线程静态字段存储提供给 Scope<T> 构造函数的实例的 Stack<T>。每次构造 Scope<T> 时,会从 Instances 属性中检索线程的 Stack<T>,并将新 T 压入其中。静态 Current 属性使用 Stack<T>.Peek 方法返回位于堆栈顶端的 T 实例(如果存在),如果 Stack<T> 为空则返回空。当删除 Scope<T> 时,顶端项将从堆栈弹出。

如果您再回头看看图 1,您可能想知道,对于 Stack<T> 字段 _instances,我为什么使用了延迟初始化。要知道,必须创建该实例,Scope<T> 构造函数才能成功运行,因此延迟初始化矫枉过正了,不是吗?当然不是,我本可以删除延迟初始化,然后只需更改该字段的声明将其初始化包括在内:

private static Stack<T> Instances { return _instances; }
[ThreadStatic]
private static Stack<T> _instances = new Stack<T>();

然而,如果进行上述更改并在多个线程中使用了 Scope<T>,则当我试图构造 Scope<T> 的新实例时,我可能会开始看到抛出的 NullReferenceException。尽管该字段已用 ThreadStatic 属性进行标记,但该字段的初始化实际上是类型的静态构造函数的一部分,因此它只执行一次并只在运行类型的静态构造函数的线程(第一个赢得其状态访问权限的线程)上执行。该线程上使用的所有 Scope<T> 均可正常工作,因为 _instances 字段已经进行了正确的初始化,但是在所有其他线程上,_instances 将仍为空,从而导致在每次 Scope<T> 的构造函数试图将某项压入不存在的堆栈时均会引发 NullReferenceException。

以下是您应该注意的其他几项实现细节。第一,将实例从堆栈弹出后,Dispose 方法将检查堆栈中是否还存在任何剩余的实例。如果不存在,它会将线程静态 Stack<T> 字段设置为空。线程静态字段具有一个很好的属性,即一旦托管线程消失,就不会再将它们视为 GC 根。不过,如果使用 Scope<T> 的线程偶尔具有很长的生存期,则所包含的 Stack<T> 被保留的时间可能会大大超过所需时间。如果随时间推移,有大量线程使用 Scope<T>,则可能导致膨胀。因此,我建议仅当堆栈中包含实例时才向其中压入实例,当堆栈为空时立即将堆栈释放。

其他需要注意的事项是对 Thread 类的静态 BeginThreadAffinity 和 EndThreadAffinity 方法的调用。之所以存在这两个方法是因为 .NET Framework 2.0 最初允许 CLR 宿主提供其自身的线程管理,允许宿主将托管线程作为纤程运行以及提供其自身的纤程调度。这样,CLR 宿主可随时将正在执行的任务从一个物理 OS 线程移动到另外一个。但根据托管代码所处理的具体内容的不同,正在执行的任务可能具有必需的线程关联度,这意味着在任务执行期间,它们可能需要位于同一物理 OS 线程上。因此,引入了 BeginThreadAffinity 和 EndThreadAffinity 方法,以便在请求线程关联度时,允许托管代码通知宿主不能移动任务。由于 Scope<T> 使用的是线程局部存储(其与物理线程而非纤程相关联),因此使用这两个方法是很重要的。否则,当执行一个线程时,Scope<T> 可以将数据存储到线程静态中,但在不为 Scope<T> 所知的情况下,任务就可能被移动到具有完全不同线程静态值的另一 OS 线程中。实际上,已经不赞成对纤程的支持,因此当前没有正确调用这两个方法不会产生任何危害。也就是说,在 Framework 将来的版本中很有可能会重新引入对纤程的支持,因此您现在应该养成使用这两个方法的习惯,以避免以后在调试时非常头疼。

最后,请注意 Scope<T> 不但将提供的实例压入了 Stack<T>,还将其存储在实例字段中。这完全是用来验证 Scope<T> 的作用域是否正确及其清理的顺序是否正确。将实例从堆栈弹出后,该实例会与 Scope<T> 中缓存的实例进行比较;这两个实例应该始终相同。如果不相同,则删除嵌套 Scope<T> 实例的顺序错误。

当然,既然我们已将用户提供的实例存储在 Scope<T> 中,那么是否还确实需要 Stack<T>?实际上,我们可以将其取消,只需将嵌套的 Scope<T> 实例链接到一起成为一个链接列表,将列表头存储在线程静态中,每个 Scope<T> 均存储对前一 Scope<T> 的引用。这不仅简化了设计,而且还删除了一些与维护 Stack<T> 相关的当前不必要分配。图 3 显示了更新的 Scope<T> 版本。

问 我喜欢 .NET Framework 中的新 Semaphore 类,但是它没有 Monitor 类(其中 C# 和 Visual Basic® 提供了 lock 和 SyncLock 关键字)易用。为什么 Semaphore 没有提供相似的功能呢?

答 通过一小段代码,它就会变得同样简单了。图 4 包含了一个轻量级包装类,您可以使用它模拟 lock/SyncLock 行为,只是用 Semaphore 替代了 Monitor。Disposable.Lock 是接受 Semaphore 的静态方法,等待 Semaphore,然后返回用于实现 IDisposable 的类的新实例;以此实例调用 Dispose 来释放 Semaphore。以下这段代码允许您对 Semaphore 使用 using 关键字,以便等待它,执行一些工作,然后将其释放:

using(Disposable.Lock(theSemaphore))
{
    ... // 在此做工作
}

图 4 中的实现很简单,但是它使用了两个您可能不熟悉的方法:Thread.BeginCriticalRegion 和 Thread.EndCriticalRegion。临界区是指在此区域内异步或未处理异常的影响可能不仅局域于当前任务,而且还可能造成整个 AppDomain 不稳定。例如,在处理一些跨线程数据时,某线程可能被中断,并且可能没有机会将一切重置为有效状态。如果在临界区中出现故障(如 ThreadAbortException),CLR 宿主(如 SQL Server™ 2005)可以选择停止整个 AppDomain,而并不冒险在可能不稳定的状态下继续执行。当线程进入或退出临界区时需要通知 CLR,以便在出现故障情况时,CLR 能够相应地通知宿主。这就是 Thread.BeginCriticalRegion 和 Thread.EndCriticalRegion 存在的原因。既然锁定用于线程间通信,那么取消锁定会开启临界区,而释放锁定会结束临界区,这是行得通的。此逻辑内置于 Monitor 中,因此在图 4 中我已将其包括在 Disposable.Lock 实现之中。有关详细信息,请参阅 MSDN® 杂志 2005 年 10 月刊中我的一篇文章,网址为 msdn.microsoft.com/msdnmag/issues/05/10/Reliability

请将您的疑问和意见通过netqa@microsoft.com 发送给 Stephen。

Stephen Toub 是 MSDN 杂志的技术编辑。

本文摘自 2006 年 9 月发行的 MSDN 杂志

转到原英文页面

posted @ 2010-02-08 13:23  zffl  阅读(273)  评论(0编辑  收藏