问 我一直在使用 .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
应几乎始终严格限制环境属性(如 Scope
下面的示例应该可以帮助您巩固上面介绍的内容。图 2 显示了如何使用具有 StreamWriter 实例的 Scope
在内部,Scope
如果您再回头看看图 1,您可能想知道,对于 Stack
private static StackInstances { return _instances; } [ThreadStatic] private static Stack _instances = new Stack ();
然而,如果进行上述更改并在多个线程中使用了 Scope
以下是您应该注意的其他几项实现细节。第一,将实例从堆栈弹出后,Dispose 方法将检查堆栈中是否还存在任何剩余的实例。如果不存在,它会将线程静态 Stack
其他需要注意的事项是对 Thread 类的静态 BeginThreadAffinity 和 EndThreadAffinity 方法的调用。之所以存在这两个方法是因为 .NET Framework 2.0 最初允许 CLR 宿主提供其自身的线程管理,允许宿主将托管线程作为纤程运行以及提供其自身的纤程调度。这样,CLR 宿主可随时将正在执行的任务从一个物理 OS 线程移动到另外一个。但根据托管代码所处理的具体内容的不同,正在执行的任务可能具有必需的线程关联度,这意味着在任务执行期间,它们可能需要位于同一物理 OS 线程上。因此,引入了 BeginThreadAffinity 和 EndThreadAffinity 方法,以便在请求线程关联度时,允许托管代码通知宿主不能移动任务。由于 Scope
最后,请注意 Scope
当然,既然我们已将用户提供的实例存储在 Scope
问 我喜欢 .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。
http://www.microsoft.com/china/MSDN/library/netFramework/netframework/NETMattersSep.mspx