为什么锁(这){…}不好?

MSDN文档说

public class SomeObject { public void SomeOperation() { lock(this) { //Access instance variables } } } 

如果实例可以被公开访问,则是“一个问题”。 我想知道为什么? 是否因为锁会比所需的时间长? 还是有一些更阴险的原因?

在locking语句中使用它是不好的forms,因为它通常不在你的控制之下,谁能locking那个对象。

为了正确地计划并行操作,应该特别注意考虑可能的死锁情况,并且具有未知数量的锁入口点阻碍了这一点。 例如,任何引用该对象的对象都可以locking,而对象devise者/创build者不知道该对象。 这增加了multithreading解决scheme的复杂性,并可能影响其正确性。

私人领域通常是一个更好的select,因为编译器将强制执行访问限制,并且将封装locking机制。 使用this违反了封装,将部分locking实现暴露给公众。 除非有logging,否则你也不清楚是否会获得这个locking。 即使如此,依靠文档来防止问题是次优的。

最后,有一个常见的错误认识, lock(this)实际上修改了作为parameter passing的对象,并以某种方式使其成为只读或不可访问的。 这是错误的 。 作为parameter passing给对象的对象仅仅作为一个 。 如果该钥匙上已经有一把锁,则不能进行locking; 否则,允许locking。

这就是为什么在lock语句中使用string作为键是不好的,因为它们是不可变的,可以在应用程序的各个部分之间共享/访问。 你应该使用一个私有variables,一个Object实例将会很好地工作。

以下面的C#代码为例。

 public class Person { public int Age { get; set; } public string Name { get; set; } public void LockThis() { lock (this) { System.Threading.Thread.Sleep(10000); } } } class Program { static void Main(string[] args) { var nancy = new Person {Name = "Nancy Drew", Age = 15}; var a = new Thread(nancy.LockThis); a.Start(); var b = new Thread(Timewarp); b.Start(nancy); Thread.Sleep(10); var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 }; var c = new Thread(NameChange); c.Start(anotherNancy); a.Join(); Console.ReadLine(); } static void Timewarp(object subject) { var person = subject as Person; if (person == null) throw new ArgumentNullException("subject"); // A lock does not make the object read-only. lock (person.Name) { while (person.Age <= 23) { // There will be a lock on 'person' due to the LockThis method running in another thread if (Monitor.TryEnter(person, 10) == false) { Console.WriteLine("'this' person is locked!"); } else Monitor.Exit(person); person.Age++; if(person.Age == 18) { // Changing the 'person.Name' value doesn't change the lock... person.Name = "Nancy Smith"; } Console.WriteLine("{0} is {1} years old.", person.Name, person.Age); } } } static void NameChange(object subject) { var person = subject as Person; if (person == null) throw new ArgumentNullException("subject"); // You should avoid locking on strings, since they are immutable. if (Monitor.TryEnter(person.Name, 30) == false) { Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\"."); } else Monitor.Exit(person.Name); if (Monitor.TryEnter("Nancy Drew", 30) == false) { Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!"); } else Monitor.Exit("Nancy Drew"); if (Monitor.TryEnter(person.Name, 10000)) { string oldName = person.Name; person.Name = "Nancy Callahan"; Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name); } else Monitor.Exit(person.Name); } } 

控制台输出

 'this' person is locked! Nancy Drew is 16 years old. 'this' person is locked! Nancy Drew is 17 years old. Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew". 'this' person is locked! Nancy Smith is 18 years old. 'this' person is locked! Nancy Smith is 19 years old. 'this' person is locked! Nancy Smith is 20 years old. Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining! 'this' person is locked! Nancy Smith is 21 years old. 'this' person is locked! Nancy Smith is 22 years old. 'this' person is locked! Nancy Smith is 23 years old. 'this' person is locked! Nancy Smith is 24 years old. Name changed from 'Nancy Drew' to 'Nancy Callahan'. 

因为如果人们可以得到你的对象实例(即:你的this )的指针,那么他们也可以尝试locking同一个对象。 现在他们可能不知道你在内部锁住了,所以这可能会导致问题(可能是死锁)

除此之外,这也是不好的做法,因为它locking“太多”

例如,你可能有一个List<int>的成员variables,而你实际上需要locking的唯一东西就是该成员variables。 如果你在你的函数中locking整个对象,那么其他调用这些函数的东西将被阻塞,等待locking。 如果这些函数不需要访问成员列表,则会导致其他代码等待并放慢您的应用程序,无缘无故。

看看MSDN主题线程同步(C#编程指南)

一般来说,最好避免在公共types或应用程序无法控制的对象实例上进行locking。 例如,如果可以公开访问实例,那么lock(this)会有问题,因为超出控制范围的代码也可能会locking对象。 这可能会造成两个或多个线程等待释放相同对象的死锁情况 。 与公共数据types相对,由于相同的原因,locking在公共数据types上可能会导致问题。 由于文本string是由公共语言运行库(CLR)实现的,因此locking文字string尤其有风险。 这意味着对于整个程序来说,有一个给定string文字的实例,完全相同的对象表示所有正在运行的应用程序域中的字面值。 因此,应用程序进程中放置在具有相同内容的string的锁将locking该应用程序中该string的所有实例。 因此,最好locking一个私人或受保护的成员,而不是实习生。 有些类专门为locking提供成员。 例如,数组types提供了SyncRoot。 许多集合types也提供一个SyncRoot成员。

我知道这是一个古老的线程,但是因为人们仍然可以查看并依赖它,所以指出lock(typeof(SomeObject))lock(this)严重得多。 话说回来; 诚挚的感谢艾伦指出lock(typeof(SomeObject))是不好的做法。

System.Type一个实例是最通用的,粗粒度的对象之一。 至less,System.Type的一个实例是AppDomain的全局实例,.NET可以在AppDomain中运行多个程序。 这意味着,如果两个完全不同的程序都试图在同一个types实例上获取同步锁,则可能会造成彼此干扰,甚至造成死锁。

所以lock(this)不是特别健壮的forms,可以引起问题,应该总是引起所有引用的原因。 然而,广泛使用,相对受到尊重,并且显然是稳定的代码,比如log4net,它广泛地使用了锁(this)模式,尽pipe我个人更喜欢看到模式的改变。

但是lock(typeof(SomeObject))开辟了一个全新的蠕虫增强jar。

物有所值。

…而且这个构造也适用同样的论点:

 lock(typeof(SomeObject)) 

想象一下,你的办公室里有一个熟练的秘书,这是该部门的共享资源。 有一段时间,你因为有一个任务而冲向他们,只是希望你的另一个同事还没有要求他们。 通常你只需要等待一段时间。

因为关怀是分享的,您的经理决定客户也可以直接使用秘书。 但是这有一个副作用:当你为这个客户工作的时候,一个客户甚至可能会要求他们,你也需要他们来执行部分任务。 发生死锁,因为声明不再是一个层次结构。 这可以通过不让客户首先要求他们来避免。

lock(this)是不好的,因为我们已经看到了。 外部的对象可能会locking对象,因为你不能控制谁在使用这个类,所以任何人都可以locking它…上面描述了一个确切的例子。 同样,解决办法是限制物体的暴露。 然而,如果你有一个privateprotectedinternal类,你可以控制谁locking你的对象 ,因为你确定自己写了你的代码。 所以这里的信息是:不要公开它。 另外,确保在类似的场景中使用locking可以避免死锁。

完全相反的是locking整个应用程序域共享的资源 – 最坏的情况。 这就像把你的秘书放在外面,让每个人都在外面领取。 结果是完全混乱 – 或者在源代码方面:这是一个坏主意; 扔掉,重新开始。 那么我们该怎么做呢?

types在应用程序域中共享,因为大多数人在这里指出。 但是我们可以使用更好的东西:string。 原因是string汇集 。 换句话说,如果您在应用程序域中有两个具有相同内容的string,则有可能它们具有完全相同的指针。 由于指针被用作锁键,所以你基本上得到的是“准备未定义的行为”的同义词。

同样,你不应该lockingWCF对象,HttpContext.Current,Thread.Current,Singletons(一般)等。最简单的方法来避免所有这一切? private [static] object myLock = new object();

如果您locking共享资源,locking指针可能会很糟糕 。 共享资源可以是计算机上的一个静态variables或一个文件,也就是类的所有用户之间共享的资源。 原因是每次你的类被实例化的时候,这个指针都会包含对内存中一个位置的不同的引用。 所以,在类的一次实例中locking它不同于在类的另一个实例中locking实例。

看看这个代码,看看我的意思。 在控制台应用程序中将以下代码添加到主程序中:

  static void Main(string[] args) { TestThreading(); Console.ReadLine(); } public static void TestThreading() { Random rand = new Random(); Thread[] threads = new Thread[10]; TestLock.balance = 100000; for (int i = 0; i < 10; i++) { TestLock tl = new TestLock(); Thread t = new Thread(new ThreadStart(tl.WithdrawAmount)); threads[i] = t; } for (int i = 0; i < 10; i++) { threads[i].Start(); } Console.Read(); } 

创build一个新的类,如下所示。

  class TestLock { public static int balance { get; set; } public static readonly Object myLock = new Object(); public void Withdraw(int amount) { // Try both locks to see what I mean // lock (this) lock (myLock) { Random rand = new Random(); if (balance >= amount) { Console.WriteLine("Balance before Withdrawal : " + balance); Console.WriteLine("Withdraw : -" + amount); balance = balance - amount; Console.WriteLine("Balance after Withdrawal : " + balance); } else { Console.WriteLine("Can't process your transaction, current balance is : " + balance + " and you tried to withdraw " + amount); } } } public void WithdrawAmount() { Random rand = new Random(); Withdraw(rand.Next(1, 100) * 100); } } 

这是一个locking程序的运行。

  Balance before Withdrawal : 100000 Withdraw : -5600 Balance after Withdrawal : 94400 Balance before Withdrawal : 100000 Balance before Withdrawal : 100000 Withdraw : -5600 Balance after Withdrawal : 88800 Withdraw : -5600 Balance after Withdrawal : 83200 Balance before Withdrawal : 83200 Withdraw : -9100 Balance after Withdrawal : 74100 Balance before Withdrawal : 74100 Withdraw : -9100 Balance before Withdrawal : 74100 Withdraw : -9100 Balance after Withdrawal : 55900 Balance after Withdrawal : 65000 Balance before Withdrawal : 55900 Withdraw : -9100 Balance after Withdrawal : 46800 Balance before Withdrawal : 46800 Withdraw : -2800 Balance after Withdrawal : 44000 Balance before Withdrawal : 44000 Withdraw : -2800 Balance after Withdrawal : 41200 Balance before Withdrawal : 44000 Withdraw : -2800 Balance after Withdrawal : 38400 

这是在myLock上locking程序的一个运行。

 Balance before Withdrawal : 100000 Withdraw : -6600 Balance after Withdrawal : 93400 Balance before Withdrawal : 93400 Withdraw : -6600 Balance after Withdrawal : 86800 Balance before Withdrawal : 86800 Withdraw : -200 Balance after Withdrawal : 86600 Balance before Withdrawal : 86600 Withdraw : -8500 Balance after Withdrawal : 78100 Balance before Withdrawal : 78100 Withdraw : -8500 Balance after Withdrawal : 69600 Balance before Withdrawal : 69600 Withdraw : -8500 Balance after Withdrawal : 61100 Balance before Withdrawal : 61100 Withdraw : -2200 Balance after Withdrawal : 58900 Balance before Withdrawal : 58900 Withdraw : -2200 Balance after Withdrawal : 56700 Balance before Withdrawal : 56700 Withdraw : -2200 Balance after Withdrawal : 54500 Balance before Withdrawal : 54500 Withdraw : -500 Balance after Withdrawal : 54000 

这里也有一些很好的讨论: 这是一个互斥体的正确使用?

关于它的非常好的文章Rico Mariani,Microsoft®.NET运行时的性能架构师, http ://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects

摘抄:

这里的基本问题是,你不拥有types对象,你不知道还有谁可以访问它。 一般来说,依靠locking一个你没有创build的对象并且不知道还有谁可能访问是一个非常糟糕的主意。 这样做会导致死锁。 最安全的方法是只locking私人对象。

因为任何可以看到你的类的实例的代码块也可以locking该引用。 你想隐藏(封装)你的locking对象,以便只有需要引用它的代码才能引用它。 关键字this指的是当前的类实例,所以任何数量的东西都可以引用它,并可以使用它来做线程同步。

要清楚的是,这是不好的,因为其他一些代码块可能会使用类实例来locking,并可能阻止您的代码获得及时的locking或可能创build其他线程同步问题。 最好的情况:没有别的使用你的类的引用来locking。 中间情况:某些东西使用对类的引用来执行locking,并导致性能问题。 最坏的情况:有些东西使用你的类的引用做锁,它会导致非常糟糕的,非常微妙的,真正难以debugging的问题。

下面是一些简单的示例代码(IMO):(将在LinqPad中工作,引用以下命名空间:System.Net和System.Threading.Tasks)

 void Main() { ClassTest test = new ClassTest(); lock(test) { Parallel.Invoke ( () => test.DoWorkUsingThisLock(1), () => test.DoWorkUsingThisLock(2) ); } } public class ClassTest { public void DoWorkUsingThisLock(int i) { Console.WriteLine("Before ClassTest.DoWorkUsingThisLock " + i); lock(this) { Console.WriteLine("ClassTest.DoWorkUsingThisLock " + i); Thread.Sleep(1000); } Console.WriteLine("ClassTest.DoWorkUsingThisLock Done " + i); } } 

对不起,但我不能同意locking这可能导致死锁。 你混淆了两件事:僵持和挨饿。

  • 你不能在不中断一个线程的情况下取消死锁,所以在进入死锁后你不能出去
  • 其中一个线程完成工作后,饥饿将自动结束

这是一张图片,说明了不同之处。

结论
如果线程饥饿对您来说不是问题,您仍然可以安全地使用lock(this) 。 你仍然必须记住,当线程正在使用lock(this)使得线程饿死时,这个线程终止于一个locking了你的对象的锁,它将最终以永恒的饥饿结束。

请参考下面的链接,解释为什么lock(this)不是一个好主意。

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

所以解决scheme是添加一个私有对象,例如lockObject到类中,并将代码区域放在lock语句中,如下所示:

 lock (lockObject) { ... } 

如果可以公开访问实例,则会出现问题,因为可能有其他请求可能使用同一个对象实例。 最好使用私有/静态variables。