GUI 博士的忠告:切勿锁定类型对象!
2003 年 6 月 5 日
在进行 Internet 或基于 Windows 的开发方面,您遇到过问题或难题吗?这时,您可以求助于 GUI 博士 (drgui@microsoft.com);GUI 博士每个月会两次做客 MSDN,在线回答您的问题。虽然博士忙碌的工作安排使他无法回复所有的问题,但他会尽可能地在这里回答更多的问题。如果恰好选中了您的问题,那么博士会送您一件 GUI 博士 T 恤衫!
摘要:GUI 博士解释了如何避免多线程程序发生死锁的方法,那就是不要使用锁定类型对象这种虽然常见但却是错误的方法。(5 页打印页)
Dr. GUI's Bits and Bytes...
...是 GUI 博士网络日记 的标题。但遗憾的是,博士还没有为该专栏添加更多的内容,因为他到加利福尼亚和黄石公园休假去了。但您可以在这里了解他的旅程……,并了解博士最近找到的一些新闻链接。或者,您可能会喜欢阅读 Alice in Blibbetland 的故事结局。
总之,如果您有话要说,请在当天的意见栏内发表您的评论。或者给 GUI 博士写封电子邮件……
希望计算机能具有常识?
GUI 博士最近看到了几篇新文章,其中一篇是关于名为 Open Mind Commonsense 的 MIT 项目,这个项目为数据库收集常识语句,通过这些语句,计算机的人工智能应用程序可以使用常识。您甚至可以通过单击上面的链接,进行注册,然后添加简单句子的常识说明,从而将自己的常识添加到数据库中。但 GUI 博士认为,如果能添加自己的常识而无需提示句子,会更加有趣……
现在,步入我们的正题!
为什么使用 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 提供了这方面的宝贵意见。
向 GUI 博士请教
著名的问题解决专家“GUI 博士”很高兴在 Internet 和基于 Windows 的开发方面提供百科全书式的知识,使各地的开发人员从中获益。如果遇到无法解决的问题,请将您的疑问发送到 drgui@microsoft.com。虽然博士忙碌的工作安排使他无法回复所有的问题,但他会尽可能地在这里回答更多的问题。如果恰好选中了您的问题,那么博士会送您一件 GUI 博士 T 恤衫!(请注意:问题可能经过整理,以确保语法正确,逻辑清晰。)
GUI 博士提供的知识还不够多?请阅读 GUI .NET 博士专栏文章!
有关博士提供的其他知识,请阅读 MSDN 库中每月发布两次的 Dr. GUI .NET 专栏文章。