Saturday, April 23, 2005

 

Lock, critical section and threadsafe in DotNet

1.
C# 中 lock 关键字的实现
By flier

刚刚在这篇文章 《How is lock keyword of C# implemented? 》http://blogs.msdn.com/junfeng/archive/2004/02/18/75454.aspx 中看到MS内部关于C#的lock关键字实现的一个讨论。

以下为引用:

Subject: RE: How is lock keyword of C# implemented?

At the core, it’s typically one ?lock cmpxchg“ instruction (for x86) for entry, and one for exit, plus a couple dozen other instructions, all in user mode. The lock prefix is replaced with a nop on uniprocessor machines.

The “lock cmpxchg” instruction basically stores the locking thread’s id in the object header, so another thread that tries to lock the same object can see that it’s already locked.

The actual implementation is a lot more complicated, of course – we use the object header for other purposes, for example, so this must be detected and dealt with, plus when a thread leaves the lock, we must detect whether other threads are waiting and so on…

Thanks

回想起前两天分析过的临界区实现,就顺便看了看rotor这方面的实现代码,发现和Windows中临界区的实现思路基本上相同。
在rotor中,每个引用对象内部实现是一个Object对象(sscliclrsrcvmobject.h:126)的实例。而对象同步机制的实现,则是通过和Object对象绑定的ObjHeader对象(sscliclrsrcvmsyncblk.h:539)中的SyncBlock结构完成的。这种实现思路跟Delphi中的VMT的实现很相似,rotor中Object对象指针的-4偏移处存储绑定的ObjHeader对象,Delphi则在负偏移处保存VMT表。

以下为引用:

class Object
{
//...

// Access the ObjHeader which is at a negative offset on the object (because of
// cache lines)
ObjHeader *GetHeader()
{
return ((ObjHeader *) this) - 1;
}

// retrieve or allocate a sync block for this object
SyncBlock *GetSyncBlock()
{
return GetHeader()->GetSyncBlock();
}

//...
};

ObjHeader::GetSyncBlock(syncblk.cpp:1206)方法从缓冲区获取或者创建新的SyncBlock对象。SyncBlock对象则是一个使用lazily created策略的可缓存结构,调用其Monitor完成对象的实际锁定工作。

以下为引用:

// this is a lazily created additional block for an object which contains
// synchronzation information and other "kitchen sink" data

class SyncBlock
{
//...

AwareLock m_Monitor; // the actual monitor

void EnterMonitor()
{
m_Monitor.Enter();
}

//...
};

AwareLock类型是一个很类似临界区的轻量级同步对象,其Enter(syncblk.cpp:1413)方法使用FastInterlockCompareExchange函数尝试锁定此Monitor。如果无法锁定则判断此Monitor的所有者线程是否是当前线程:是则调用线程嵌套锁定函数;否则等待此对象锁定状态的改变。

以下为引用:

Thread *pCurThread = GetThread();

for (; {

// Read existing lock state.
LONG state = m_MonitorHeld;

if (state == 0) {

// Common case: lock not held, no waiters. Attempt to acquire lock by
// switching lock bit.
if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, 1, 0) == 0)
break;

} else {

// It's possible to get here with waiters but no lock held, but in this
// case a signal is about to be fired which will wake up a waiter. So
// for fairness sake we should wait too.
// Check first for recursive lock attempts on the same thread.
if (m_HoldingThread == pCurThread)
goto Recursion;

// Attempt to increment this count of waiters then goto contention
// handling code.
if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, state + 2, state) == state)
goto MustWait;
}

}

可以看到这儿的实现思路和临界区的实现基本上相同。
FastInterlockCompareExchange函数(util.hpp:66)则是MS那个讨论里面提到的lock cmpxchg指令的调用之处。此函数根据编译时选项,被替换成CompareExchangeUP/CompareExchangeMP两个函数分别处理单/多处理器情况。可以参考vmi386cgenx86.cpp中的InitFastInterlockOps函数(cgenx86.cpp:2106)实现。在386平台上,这两个函数完全由汇编语言实现(i386asmhelpers.asm:366, 440)。

以下为引用:

CmpXchgOps FastInterlockCompareExchange = (CmpXchgOps)CompareExchangeUP;

// Adjust the generic interlocked operations for any platform specific ones we
// might have.
void InitFastInterlockOps()
{
_ASSERTE(g_SystemInfo.dwNumberOfProcessors != 0);

if (g_SystemInfo.dwNumberOfProcessors != 1)
{
//...

FastInterlockCompareExchange = (CmpXchgOps)CompareExchangeMP;

//...
}
}

以下为引用:

FASTCALL_FUNC CompareExchangeUP,12
_ASSERT_ALIGNED_4_X86 ecx
mov eax, [esp+4] ; Comparand
cmpxchg [ecx], edx
retn 4 ; result in EAX
FASTCALL_ENDFUNC CompareExchangeUP

FASTCALL_FUNC CompareExchangeMP,12
_ASSERT_ALIGNED_4_X86 ecx
mov eax, [esp+4] ; Comparand
lock cmpxchg [ecx], edx
retn 4 ; result in EAX
FASTCALL_ENDFUNC CompareExchangeMP

值得注意的是那个讨论里面提到“The lock prefix is replaced with a nop on uniprocessor machines”,据rain的分析,NT核心部分的DLL也做了类似的优化。

2.
切勿锁定类型对象

MTT翻译组翻译

http://blog.vckbase.com/song/articles/444.aspx

为什么使用 Lock(typeof(ClassName)) 或 SyncLock GetType(ClassName) 是错误的

最近,Microsoft .NET 运行库的性能设计师及资深 Microsoft 开发人员 Rico Mariani 在一封电子邮件中与 GUI 博士进行了交流,其中提到的一种相当普遍的做法(遗憾的是,这种做法在我们的一些文档中也曾提到过,虽然我们将进行修改)实际上却存在着很大的问题。他询问 GUI 博士能否帮忙发布消息,告诉程序员不应该采用这种做法。博士当然很乐意帮忙。

这种非常普遍的做法是什么呢?其实就是对类型对象加锁。在 C# 中,加锁的做法是 lock(typeof(ClassName)),其中,ClassName 是某个类的名称;在 Microsoft Visual Basic .NET 中,加锁的做法是 SyncLock GetType(ClassName)。

背景知识:在多线程编程中,lock/SyncLock 语句用于创建代码中一次只执行一个线程的关键部分或简要部分。(如果您需要同时更新对象中的多个字段,则可能需要该语句 — 您希望确保其他线程不会同时尝试更新该对象!)此语句将锁定与您指定的对象相关联的唯一监视对象,如果其他线程已经锁定了该监视对象,则等待。一旦它锁定了监视对象,任何其他线程都无法锁定该监视对象,除非您的线程解除锁定,解除锁定会在封闭块的结尾自动发生。一种常见的用法是锁定 this/Me 引用,这样,只有您的线程可以修改您在使用的对象 — 不过,更好的做法是锁定您即将修改的特定对象。锁定尽可能小的对象的好处是可以避免不必要的等待。

GetType 和 typeof 返回对该类型的类型对象的引用。System.Type 类型的类型对象包含使您能够反映类型的方法,这意味着您可以找到它的字段和方法,甚至可以访问字段和调用方法。一旦您拥有对类型对象的引用,就可以创建该对象的一个实例(并且,如果您使用 Type.GetType shared/static 方法,就可以按名称获得对类型对象的引用)。

因此,类型对象非常方便。但是,有些程序员喜欢“滥用”这种方式,借此来代替可以对其进行加锁的 static/Shared 对象。(遗憾的是,我们在 C# 文档和 Visual Basic .NET 文档中都提到了这种方法,暗示这是一种建议采用的做法。)在这种情况下,这些文档中的建议是错误的(我们会进行纠正)。这种做法是不 可接受的,更不用说建议采用了。

原因是这样的:由于一个类的所有实例都只有一个类型对象,因此从表面看,锁定类型对象相当于锁定类中包含的静态对象。只要您锁定类的所有实例,等到其他线程访问完任一实例的任何部分,然后锁定访问,这样您就可以安全地访问静态成员,而不会受到其他线程的干扰。

这种做法的确有效,至少在大多数情况下是这样的。但它也有一些问题:首先,获得类型对象实际上是一个很缓慢的过程(尽管大多数程序员会认为这个过程非常快);其次,任何类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致您挂起。

这里的基本问题是,您并未拥有该类型对象,并且您不知道还有谁可以访问它。总的来说,依靠锁定不是由您创建、并且您不知道还有谁可以访问的对象是一种很不好的做法。这样做很容易导致死锁。最安全的方式就是只锁定私有对象。

但除此之外,还有更严重的问题。由于在当前版本的 .NET 运行库中,类型对象有时会在应用程序域之间(但不是在进程之间)共享。(通常这没有问题,因为它们是不变的。)这意味着,运行在其他应用程序域(但在同一进程)中的另一个应用程序有可能对您要锁定的类型对象进行加锁,并且始终不释放该类型对象,从而使您的应用程序发生死锁。并且,这样可以很容易地获得类型对象的访问权限,因为该对象具有名称 — 该类型的完全限定名!请记住,lock/SyncLock 会一直阻塞(这是挂起的含蓄说法),直到它可以获得锁定为止。很显然,依靠锁定其他程序或组件可以锁定的对象不是一种很好的做法,并且会导致死锁。

即使该类型对象在您的应用程序域中是唯一的,这仍然是一种不好的做法,因为任何代码都可以访问公共类型的类型对象,从而导致死锁的发生。如果您在应用程序中使用的组件不是您编写的,这种做法尤其成问题。(即使是 lock(this)/SyncLock Me 也可能有这个问题,因为其他人可能会锁定您。即使发生了这种事情,问题的根源也可能会比锁定类型对象而导致的死锁更容易发现,因为您的对象并不是跨应用程序域的全局可用对象。)

那么,应该采用什么方法呢?非常简单:只要声明并创建一个对象作为锁,然后使用它而不是 类型对象来进行锁定。通常,为了复制问题代码的语义,您会希望此对象是 static/Shared — 当然,它其实应该是私有的!总之,您可以将以下问题代码:

// C#
lock(typeof(Foo)) { // BAD CODE! NO! NO! NO!
// statements;
}

' VB .NET
SyncLock GetType(MyClass) ' BAD CODE! NO! NO! NO!
' statements
End SyncLock

更改为以下正确代码:

// C#
lock(somePrivateStaticObject) { // Good code!
// statements;
}

' VB .NET
SyncLock GetType(somePrivateStaticObject) ' Good code!
' statements
End SyncLock

当然,您必须已经拥有一个要锁定的私有静态对象(如果您使用锁定来修改静态对象,实际上您可能已经有了一个!)或者必须创建一个。(使它成为私有对象可以避免其他类锁定您的对象。)请不要尝试锁定不是引用(对象)类型的字段,例如 int/Integer。那样会出现编译器错误。如果您没有要锁定的私有静态对象,可能需要创建一个哑对象:

// C#
Class MyClass {
private static Object somePrivateStaticObject = new Object();
// methods of class go here--can lock somePrivateStaticObject
}

' VB .NET
Class MyClass
Private Shared somePrivateStaticObject As New Object
' methods of class go here--can lock somePrivateStaticObject
End Class

您需要单独分析每种情况,以确保不会出现问题,但通常上述技巧会奏效。

有两点需要注意:首先,类以外的任何代码都无法锁定 MyClass.somePrivateStaticObject,因此避免了许多死锁的可能。由于死锁属于那种最难找到根源的问题,因此,避免发生死锁的可能是一件很好的事情。

其次,您知道,您的应用程序中只有一份 MyClass.somePrivateStaticObject 的副本,并且系统上运行的其他每个应用程序也只有一个副本。因此,在同一个应用程序域中的应用程序之间没有相互影响。GUI 博士希望您能明白为什么修改后的代码比原来的问题代码更加可靠和强大。

总之,不要锁定类型对象,因为您并不知道哪里又出现问题了。锁定类型对象的过程很慢,并且可能发生死锁情况。这是一种很不好的编程习惯。相反,您应该在对象中锁定静态对象。

致谢!

GUI 博士在此感谢 .NET 运行库的性能设计师 Rico Mariani 提供了这方面的宝贵意见。

3.
安全的线程同步
原作:Jeffrey Richter [msdn.2003.01]
翻译:刘未鹏
出处:http://blog.csdn.net/pongba/archive/2004/08/24/83918.aspx

(按:再次复习2004/3/11-12的概念)

到目前为止,线程同步最为普遍的用途是确保多线程对共享资源的互斥访问。对于同步单个进程中的多个线程以实现互斥访问,Win32 API中的CRITICAL_SECTION结构以及与它相关的函数提供了最为快速和高效的途径,Microsoft .NET Framework并没有提供CRITICAL_SECTION结构,但提供了一个类似的机制——该机制通过System.Threading.Monitor类和SyncBlock得以实现。

在这个专栏里,我会解释.NET Framework如何支持线程同步的这个普遍用途。另外,我还要解释SyncBlock和Monitor被设计成现在这个样子的动机以及它们是如何工作的。最后我还要解释为什么这个设计是糟糕的,以及如何用正确和安全的方式去使用该机制。



绝妙的主意



.NET Framework采用了OOP式的结构。这就意味着:开发者构造对象,然后调用类型的成员来操纵它。然而有时候这些对象也会被多个线程所操纵,因此,为了确保对象的状态不被破坏,必须进行线程同步。在设计.NET Framework时,Microsoft的设计师们决定创造一个新的机制来让开发者们轻易地同步一个对象。

基本想法是这样的:堆上的每个对象都关联着一个可以被用于线程同步的数据结构(非常类似于Win32的CRITICAL_SECTION)。然后,FCL(framework class library,框架类库)再提供一些方法(method)——当你将对象的引用传递过去时——使用这个数据结构来同步线程。

如果将这个设计运用在Win32下的非托管的C++类上,则该类看起来像这样:



图1. A CRITICAL_SECTION for Every Object

class SomeType {

private:

// 为每个对象关联一个私有的CRITICAL_SECTION字段

CRITICAL_SECTION m_csObject;



public:

SomeType() {

// 在构造函数中初始化CRITICAL_SECTION字段

InitializeCriticalSection(&m_csObject);

}



~SomeType() {

// 在析构函数中delete CRITICAL_SECTION字段

DeleteCriticalSection(&m_csObject);

}



void SomeMethod() {

// 在该函数中,我们使用了对象的CRITICAL_SECTION字段

// 来同步多个线程对该对象的访问

EnterCriticalSection(&m_csObject);

// 在这里可以执行线程安全的代码了...

LeaveCriticalSection(&m_csObject);

}



void AnotherMethod() {

// 在该函数中,我们使用了对象的CRITICAL_SECTION字段

// 来同步多个线程对该对象的访问

EnterCriticalSection(&m_csObject);

// 在这里可以执行线程安全的代码了...

LeaveCriticalSection(&m_csObject);

}

};



本质上,.NET Framework为每个对象关联一个类似CRITICAL_SECTION的字段,并且负责对它进行初始化和删除。开发者要做的唯一一件事情是:在需要线程同步的方法中加入一些代码以进入(Enter)和离开(Leave)该字段。



实现绝妙的主意



现在,很明显,为堆上的每个对象都关联一个CRITICAL_SECTION字段是一件很浪费的事情,特别是由于大多数对象从不需要线程安全的访问。因此,.NET Framework小组设计了一个更高效的途径来提供前面描述的功能。下面就来说说它是如何工作的:

当CLR(公共语言运行时)初始化时,它分配一个SyncBlock的缓存区。一个SyncBlock就是一个可以根据需要关联到任何对象的内存块。SyncBlock具有与Win32 CRITICAL_SECTION相同的字段。

堆上的每个对象在创建时都关联了两个额外的字段——第一个是MethodTablePointer(方法表指针),包含了该类型的方法表的地址。基本上,这个指针使你能够获取堆上的每个对象的类型信息。事实上,当你调用System.Object的GetType方法时,该方法通过对象的MethodTablePointer字段来确定对象的类型。另一个额外的字段——称为SyncBlockIndex——包含一个32位的有符号整数,在SyncBlock缓存区中索引一个SyncBlock。

当一个对象被构造时,它的SyncBlockIndex字段会被初始化为一个负值——表示根本不指向任何SyncBlock。然后,当某个方法被调用以同步对该对象的访问时,CLR在SyncBlock缓存区中寻找一个空闲的SyncBlock,并且让该对象的SyncBlockIndex字段指向它。换句话说,SyncBlock只在某个对象需要同步字段时才会被关联到该对象。而当不再有任何线程对该对象进行同步访问时,该对象的SyncBlockIndex字段就会被重置为一个负值,并且相应的SyncBlock也被释放,以便在将来可以被重新关联到其它对象。

图2是这个思想的具体表现形式。在该图的CLR Data Structures部分,你可以看到,系统所知道的每个类型都有一个对应的数据结构。你也可以看到SyncBlock结构的集合。在该图的Managed Heap部分你可以看到有三个对象(ObjectA,ObjectB,ObjectC)被创建了。每个对象的MethodTablePointer字段都指向相应类型的方法表。通过方法表,你可以获得每个对象的类型。所以,我们可以很容易的看到,ObjectA和ObjectB都是SomeType类型的实例,而ObjectC是AnotherType类型的实例。

ObjectA的SyncBlockIndex字段被设为0,这表示SyncBlock #0目前正被ObjectA使用。另一方面,ObjectB的SyncBlockIndex被设为-1,表示并没有任何SyncBlock与它关联。最后,ObjectC的SyncBlockIndex为2,表明它正在使用SyncBlock #2。在本例中,SyncBlock #1没有被使用,但是将来可能会与某个对象关联。

因此,从逻辑上说,堆上的每个对象都关联着一个SyncBlock——它可以被用于快速互斥的线程同步。然而,从物理上说,SyncBlock仅当被需要时才会与某个对象关联,并且在对象不再需要它时从对象上脱离。这意味着内存的使用是有效率的。另外,如果有必要,SyncBlock缓存可以创建更多的SyncBlock,因此你不用担心它会由于同一时刻同步对象过多而被用尽。



使用Monitor来操纵SyncBlock


既然你已经理解了SyncBlock,就让我们来看看如何锁定一个对象。要锁定或解锁一个对象,你可以使用System.Threading.Monitor类。该类型的所有方法都是静态的。调用下面的方法可以锁定一个对象:



public static void Enter(object obj);



当你调用Enter方法时,它首先检查指定的对象的SyncBlockIndex是否为负值,如果是的,则该方法从SyncBlock缓存中找一个空闲的SyncBlock并且将它的索引保存到对象的SyncBlockIndex中。一旦对象上已经关联了SyncBlock,则Enter方法就会检查对象的SyncBlock,看看当前是否有另一个线程正拥有这个SyncBlock。如果当前没有任何进程拥有它,则当前调用Enter的线程成为该对象的所有者。另一方面,如果另一个线程已经拥有了该SyncBlock,则当前调用Enter的线程会挂起,直到那个线程释放对SyncBlock的所有权。

如果你想写更具防御性的代码,则可以调用下面的TryEnter方法之一:



public static Boolean TryEnter(object obj);



public static Boolean TryEnter(object obj,

int millisecondsTimeout);



public static Boolean TryEnter(object obj,

TimeSpan timeout);



第一个TryEnter只是简单的检查调用它的线程能否获得对象的SyncBlock的所有权,如果成功则返回true。另外两个方法则允许你指定一个等待时间,表示你允许线程等待所有权多久。如果不能获得所有权,则三个方法都会返回false。

一旦获得了所有权,代码就可以安全地访问对象的字段了。访问完毕之后,线程还应该调用Exit来释放SyncBlock:



public static void Exit(object obj);



如果线程并没有获得指定对象的SyncBlock的所有权,则调用Exit会抛出一个SynchronizationLockException。同时还要注意,线程可以递归式获取一个SyncBlock的所有权,每次成功的Enter必须对应一个Exit,这样最终SyncBlock才会被释放(这一点与Win32的CRITICAL_SECTION是一样的——译者)。



Synchronizing the Microsoft Way



现在让我们看看图3的示例代码,它展示了如何使用Monitor的Enter和Exit方法来锁定和解锁一个对象。注意,LastTransaction属性(property)的实现需要调用Enter,Exit以及一个临时变量dt。这是非常重要的,这样可以避免返回一个已经被破坏的值——如果一个线程调用PerformTransaction时另一个线程在访问该属性就会发生这种情况。



用C#的Lock语句来简化代码



因为这种“调用Enter——访问受保护资源——调用Exit”的模式是如此普遍,所以C#干脆提供了特殊的语法来简化这种代码。图4的两段C#代码片断功能相同,但是后者更为简洁。使用C#的lock语句,你可以将Transaction类充分简化。特别要注意图5中所展示的改进后的LastTransaction属性,它不再需要临时变量了。

除了能够简化代码外,lock语句还确保了Monitor.Exit一定会被调用,从而即使在try块中产生了一个异常,SyncBlock也会被释放。你应该始终将异常处理与同步机制结合起来使用,以确保锁被正确释放。然而,如果你使用C#的lock语句,则编译器会自动帮你生成正确的代码。另外,Visual Basic .NET也有一个类似于C#的lock语句的SyncLock语句,它们做同样的事情。



Synchronizing Static Members the Microsoft Way



Transaction类示范了如何同步访问一个对象的实例字段。但是,如果你的类型定义了一些静态字段以及访问这些字段的静态函数又当如何呢?在这种情况下,堆上并没有该类型的实例,因而也就没有可用的SyncBlock或者传递给Monitor.Enter和Monitor.Exit方法的对象引用。

事实上,包含某个类型的类型描述符的内存块是位于堆上的对象。图2中并没有表现出这一点,但是SomeType的类型描述符和AnotherType的类型描述符所占的内存块其实本身都是对象,并且同样也有MethodTablePointer字段和SyncBlockIndex字段。这就意味着SyncBlock可以被关联到一个类型,并且类型对象(type object,指“描述一个类型”的对象)的引用可以被传递给Monitor的Enter和Exit方法。在图6所示的Transaction类中,所有的成员都改成了静态的,并且PerformTransaction方法和LastTransaction属性也作了改动以展示Microsoft希望开发者如何同步对静态成员的访问。

在PerformTransaction方法和LastTransaction属性中,你不再会看到this关键字,因为在静态成员中不能使用它。我将类型的类型描述符对象的引用传给lock语句。这个引用是通过C#的typeof操作符得到的,typeof操作符返回指定对象的对象描述符的引用。在Visual Basic .NET中,GetType操作符具有同样的功能。



为什么绝妙的主意并不那么绝妙



如你所见,使堆上的每个对象逻辑上关联一个用于同步的数据结构的主意听起来很不错。但是实际上这是一个糟糕的主意。听我解释原因。还记得在本文开头展示的非托管的C++代码吗?如果由你来写,你会将CRITICAL_SECTION字段的访问权限设为public吗?当然不会——那简直是荒谬的。将这个字段设为public会允许程序中的任何代码操纵该CRITICAL_SECTION结构,这样恶意代码很容易就能够将使用该类型的实例的任何线程死锁住。

呃...猜猜看发生了什么——SyncBlock正如一个public字段一样!任何一段代码在任何时候都可以将任何对象的引用传给Monitor的Enter和Exit方法。事实上,任何类型描述符的引用也同样可以被传给Monitor的方法。

图7的代码显示了这种情况是多么糟糕。这里,Main方法中创建了一个App对象,然后锁定该对象,并在某个时刻发生一次垃圾收集(在这段代码中,强制一次垃圾收集),当App的Finalize方法被调用时,它也会试图锁定该对象,但是由于程序的主线程已经锁定了该对象,所以CLR的Finalize线程无法锁定它。这就导致CLR的Finalize线程停止了——于是在当前进程(可能包含多个AppDomain)中再也不会有其它对象可以被finalize,也不再有其它可finalize的对象在堆上的内存会被回收。

幸运的是有一个解决方案,只不过那意味着你得抛开Microsoft的设计和建议。取而代之的是定义一个private的System.Object字段作为你的类型的成员,构造它,然后将其引用传递给C#的lock语句或Visual Basic .NET的SyncLock语句。图8展示了如何重写Transaction类以便让用于同步的对象成为该类的私有成员。同样地,图9展示了当Transaction类的成员全为静态时如何去重写它。

看起来,仅仅为了同步而构造一个System.Object对象是比较怪异的。我感觉Microsoft对Monitor类的设计并不恰当。应该让你为每个想要同步的类型(原文为type,疑为object)构造一个Monitor类型的实例。这样,静态方法(Monitor类的静态方法)就会成为不需要System.Object参数的实例方法。这将解决所有的问题,并且充分简化开发者的编程模型。

另外,如果你创建具有许多字段的复杂类型,则在任何时候,你的方法和属性可能只需要锁定这些字段的一个子集。你始终可以通过将指定字段的引用传给lock或Monitor.Enter来锁定它。当然,如果字段为私有(我始终建议如此),则我只会考虑这样做。如果你想要将几个字段一起锁定,那么你可以始终将其中的一个传给lock或Enter。或者,你还可以构造一个System.Object对象——它的唯一意图就是用于锁定一集字段。lock段(临界段)越细化,代码的性能和可测性就越好[1]。

[1] 译注:作者的意思是,应该保持lock(临界)段的短小,换句话说,一个lock(临界)段应该执行尽量少的代码,这样才能保证其它线程在lock(临界)区上的等待时间尽量短,并且死锁的可能性也更小。



未装箱的(unboxed)值类型实例



在结束这个专栏之前,我想要指出一个有关同步的bug,我第一次遇到它时花了好几个小时来跟踪。下面的代码片断示范了这个问题:



class AnotherType {



// 一个 未装箱的(unboxed) Boolean 值类型实例

private Boolean flag = false;


public Boolean Flag {

set {

Monitor.Enter(flag); // 将flag装箱并锁定装箱后的对象

flag = value; // 而实际的值却未受保护

Monitor.Exit(flag); // 将flag装箱并试图unlock装箱后的对象

}

}

}



你可能会惊讶于在这段代码中并没有发生任何线程同步!原因是:flag是个未装箱的值类型实例,而并非一个引用类型。未装箱的值类型实例并没有MethodTablePointer和SyncBlockIndex这两个额外的字段。这就意味着一个未装箱的值类型实例不可能有一个与它关联的SyncBlock。

Monitor的Enter和Exit方法要求一个指向堆上的对象的引用。当C#,Visual Basic .NET和许多其它编译器看到一段代码试图将未装箱的值类型实例传给需要对象引用的方法时,它们会自动生成代码来将该值类型的实例装箱(box)。装箱后的实例(位于堆上)将会拥有一个MethodTablePointer和一个SyncBlockIndex,因而可以被用于线程同步。然而,每调用这样的函数一次就会进行一次新的装箱,即产生一个新的装箱后的实例,这个实例与以前装箱的实例都不相同,也就是说,我们每次lock和unlock的都是不同的对象。

例如,在上面代码片断中,当Flag属性的set方法被调用时,它调用了Monitor的Enter方法。Enter需要一个引用类型,因此flag被装箱,并且装箱后的对象的引用被传递给Enter,该对象的SyncBlock现在归调用线程所有。如果另一个线程现在也要访问这个属性,那么flag将会被再次装箱,产生一个新的对象,它拥有自己的SyncBlock。另外,对Exit的调用也会导致一次装箱操作。

正如我前面所说,我花了好几个小时才发现问题所在。如果你想要同步对一个未装箱的值类型实例的访问,那么你必须分配一个System.Object对象,并利用它来进行同步。图10中的代码是正确的。

另外,如果你使用C#的lock语句来代替Monitor.Enter与Monitor.Exit,那么C#编译器会帮你避免意外地试图去lock一个值类型。当你将一个未装箱的值类型实例传给lock语句时,C#编译器会报错。例如,如果你试图将一个Boolean(C#中的bool)传给lock语句,那么你将看到如下的错误:“error CS0185:'bool' is not a reference type as required by the lock statement”。而在Visual Basic .NET中,如果你对SyncLock语句使用未装箱的值类型实例,编译器也会报错:“error BC30582: 'SyncLock' operand cannot be of type 'Boolean' because 'Boolean' is not a reference type”。


图 3. Using Enter and Exit Methods

class Transaction {



// Private field holding the time of

// the last transaction performed

private DateTime timeOfLastTransaction;



public void PerformTransaction() {

// Lock this object

Monitor.Enter(this);



// Perform the transaction...



// Record time of the most recent transaction

timeOfLastTransaction = DateTime.Now;



// Unlock this object

Monitor.Exit(this);

}



// Public read-only property returning

// the time of the last transaction

public DateTime LastTransaction {

get {

// Lock this object

Monitor.Enter(this);



// Save the time of the last transaction

// in a temporary variable

DateTime dt = timeOfLastTransaction;



// Unlock this object

Monitor.Exit(this);



// Return the value in the temporary variable

return(dt);

}

}

}



图 4. Regular and Simple Lock and Unlock

// Regular function

public void SomeMethod() {

// Lock the object

Object oTemp = this;

Monitor.Enter(oTemp);

try {

// Access the object

...

// Unlock the object

}

finally {

Monitor.Exit(oTemp);

}

// Return

}



// Simple function

public void SomeMethod() {

// Lock the object

lock (this) {



// Access the object

...

// Unlock the object

}



// Return

}



图 5. Transaction Class

class Transaction {



// Private field holding the time of

// the last transaction performed

private DateTime timeOfLastTransaction;



public void PerformTransaction() {

lock (this) {

// Perform the transaction...



// Record time of the most recent transaction

timeOfLastTransaction = DateTime.Now;

}

}



// Public read-only property returning

// the time of the last transaction

public DateTime LastTransaction {

get {

lock (this) {

// Return the time of the last transaction

return timeOfLastTransaction;

}

}

}

}



图 6. New Transaction Class

class Transaction {



// Private field holding the time of

// the last transaction performed

private static DateTime timeOfLastTransaction;



public static void PerformTransaction() {

lock (typeof(Transaction)) {

// Perform the transaction...



// Record time of the most recent transaction

timeOfLastTransaction = DateTime.Now;

}

}



// Public read-only property returning

// the time of the last transaction

public static DateTime LastTransaction {

get {

lock (typeof(Transaction)) {

// Return the time of the last transaction

return timeOfLastTransaction;

}

}

}

}



图 7. Threads Banging Heads

using System;

using System.Threading;



class App {

static void Main() {

// Construct an instance of the App object

App a = new App();



// This malicious code enters a lock on

// the object but never exits the lock

Monitor.Enter(a);



// For demonstration purposes, let's release the

// root to this object and force a garbage collection

a = null;

GC.Collect();



// For demonstration purposes, wait until all Finalize

// methods have completed their execution - deadlock!

GC.WaitForPendingFinalizers();



// We never get to the line of code below!

Console.WriteLine("Leaving Main");

}



// This is the App type's Finalize method

~App() {

// For demonstration purposes, have the CLR's

// Finalizer thread attempt to lock the object.

// NOTE: Since the Main thread owns the lock,

// the Finalizer thread is deadlocked!

lock (this) {

// Pretend to do something in here...

}

}

}



图 8. Transaction with Private Object

class Transaction {



// Private Object field used

// purely for synchronization

private Object objLock = new Object();



// Private field holding the time of

// the last transaction performed

private DateTime timeOfLastTransaction;



public void PerformTransaction() {

lock (objLock) {

// Perform the transaction...



// Record time of the most recent transaction

timeOfLastTransaction = DateTime.Now;

}

}



// Public read-only property returning

// the time of the last transaction

public DateTime LastTransaction {

get {

lock (objLock) {

// Return the time of the last transaction

return timeOfLastTransaction;

}

}

}

}



图 9. Transaction with Static Members

class Transaction {



// Private, static Object field

// used purely for synchronization

private static Object objLock = new Object();



// Private field holding the time of

// the last transaction performed

private static DateTime timeOfLastTransaction;



public static void PerformTransaction() {

lock (objLock) {

// Perform the transaction...



// Record time of the most recent transaction

timeOfLastTransaction = DateTime.Now;

}

}



// Public read-only property returning

// the time of the last transaction

public static DateTime LastTransaction {

get {

lock (objLock) {

// Return the time of the last transaction

return timeOfLastTransaction;

}

}

}

}



图 10. Now There's Synchronization

class AnotherType {



// An unboxed Boolean value type

private Boolean flag = false;



// A private Object field used to

// synchronize access to the flag field

private Object flagLock = new Object();



public Boolean Flag {

set {

Monitor.Enter(flagLock);

flag = value;

Monitor.Exit(flagLock);

}

}

}



<< Home

This page is powered by Blogger. Isn't yours?