Why is lock(this) {…} bad?
MSDN Documentation says
1 2 3 4 5 6 7 8 9 10 | public class SomeObject { public void SomeOperation() { lock(this) { //Access instance variables } } } |
Is"a problem if the instance can be accessed publicly".我在想为什么?是因为锁会比需要长吗?然而,是否有更多的不明原因?
在lock语句中使用
为了正确地规划并行操作,应该特别注意考虑可能的死锁情况,并且具有未知数量的锁入口点会阻碍这一点。例如,任何引用对象的人都可以在对象设计器/创建者不知道的情况下锁定它。这会增加多线程解决方案的复杂性,并可能影响它们的正确性。
私有字段通常是一个更好的选项,因为编译器将对其强制执行访问限制,并且它将封装锁定机制。使用
最后,有一种常见的误解,即
这就是为什么在
以下面的C代码为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | 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); } } |
控制台输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | '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'. |
因为如果人们可以访问您的对象实例(即:您的
除此之外,这也是不好的做法,因为它锁定的"太多"
例如,您可能有一个
查看msdn主题线程同步(C编程指南)
Generally, it is best to avoid locking
on a public type, or on object
instances beyond the control of your
application. For example, lock(this)
can be problematic if the instance can
be accessed publicly, because code
beyond your control may lock on the
object as well. This could create
deadlock situations where two or more
threads wait for the release of the
same object. Locking on a public
data type, as opposed to an object,
can cause problems for the same
reason. Locking on literal strings is
especially risky because literal
strings are interned by the common
language runtime (CLR). This means
that there is one instance of any
given string literal for the entire
program, the exact same object
represents the literal in all running
application domains, on all threads.
As a result, a lock placed on a string
with the same contents anywhere in the
application process locks all
instances of that string in the
application. As a result, it is best
to lock a private or protected member
that is not interned. Some classes
provide members specifically for
locking. The Array type, for example,
provides SyncRoot. Many collection
types provide a SyncRoot member as
well.
我知道这是一条古老的线索,但因为人们仍然可以仰视它并依赖它,所以指出
因此,
但是
为了它的价值。
…同样的参数也适用于这个结构:
1 |
想象一下,你的办公室里有一个技术熟练的秘书,这是这个部门的共同资源。有一次,你冲向他们,因为你有一个任务,只是希望你的另一个同事还没有认领他们。通常你只需要等待一小段时间。
因为关怀是共享的,所以您的经理决定客户也可以直接使用秘书。但这有一个副作用:当您为这个客户工作时,客户甚至可能会要求他们索赔,而且您还需要他们执行部分任务。死锁发生,因为声明不再是一个层次结构。这一点本来可以一起避免,因为首先不允许客户提出索赔。
如我们所见,
与此完全相反的是锁定在整个应用程序域中共享的资源——最坏的情况。这就像把你的秘书放在外面,让外面的每个人都认领他们。结果是彻底的混乱——或者从源代码的角度来说:这是个坏主意;扔掉它重新开始。那我们该怎么做呢?
正如这里大多数人指出的,类型在应用程序域中是共享的。但我们可以使用更好的东西:字符串。原因是字符串被合并。换句话说:如果在一个应用程序域中有两个内容相同的字符串,那么它们有可能拥有完全相同的指针。因为指针被用作锁定键,所以您基本上得到的是"准备未定义的行为"的同义词。
同样,您不应该锁定wcf对象、httpcontext.current、thread.current、singletons(一般情况下)等。避免所有这些的最简单方法是什么?
如果锁定共享资源,则锁定此指针可能会很糟糕。共享资源可以是计算机上的静态变量或文件,也就是类中所有用户共享的资源。原因是每次实例化类时,此指针将包含对内存中某个位置的不同引用。因此,在类的一次实例中锁定它与在类的另一个实例中锁定它不同。
查看此代码以了解我的意思。在控制台应用程序的主程序中添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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(); } |
创建如下所示的新类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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); } } |
这是一个锁定程序的运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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的程序锁定运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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在http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects上写了一篇非常好的文章。NET运行时
Excerpt:
The basic problem here is that you don't own the type object, and you
don't know who else could access it. In general, it's a very bad idea
to rely on locking an object you didn't create and don't know who else
might be accessing. Doing so invites deadlock. The safest way is to
only lock private objects.
这里有一个更简单的例子(取自这里的问题34),当类的使用者也试图锁定对象时,为什么锁(这个)是坏的,并且可能导致死锁。下面,只有三个线程中的一个可以继续,另外两个是死锁的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 class SomeClass
{
public void SomeMethod(int id)
{
**lock(this)**
{
while(true)
{
Console.WriteLine("SomeClass.SomeMethod #" + id);
}
}
}
}
class Program
{
static void Main(string[] args)
{
SomeClass o = new SomeClass();
lock(o)
{
for (int threadId = 0; threadId < 3; threadId++)
{
Thread t = new Thread(() => {
o.SomeMethod(threadId);
});
t.Start();
}
Console.WriteLine();
}
为了解决这个问题,这个人使用了thread.trymonitor(超时)而不是lock:
1
2
3
4
5
6
7
8
9 Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
if (lockWasTaken)
{
doAction();
}
else
{
throw new Exception("Could not get lock");
}
https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks
这里还有一些很好的讨论:这是互斥的正确使用吗?
请参考下面的链接,解释为什么锁(这个)不是一个好主意。
http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx
因此,解决方案是向类中添加一个私有对象,例如lock object,并将代码区域放在lock语句中,如下所示:
1 2 3 4 | lock (lockObject) { ... } |
下面是一些更简单的示例代码(IMO):(将在LinqPad中使用,引用以下命名空间:System.net和System.Threading.Tasks)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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); } } |
因为任何可以看到类实例的代码块也可以锁定该引用。您希望隐藏(封装)您的锁定对象,以便只有需要引用它的代码才能引用它。关键字this引用当前类实例,因此任何数量的事物都可以引用它,并可以使用它来进行线程同步。
显然,这是不好的,因为其他一些代码块可以使用类实例来锁定,并且可能会阻止代码获得及时的锁定,或者可能会造成其他线程同步问题。最佳情况:没有其他方法使用对类的引用来锁定。中间的情况:某些东西使用对类的引用来进行锁定,这会导致性能问题。最坏的情况是:某些东西使用类的引用来进行锁定,这会导致非常糟糕、非常微妙、非常难以调试的问题。
抱歉,伙计们,我不同意这样的论点,锁定这个可能会导致死锁。你混淆了两件事:僵局和饥饿。
- 如果不中断其中一个线程,就无法取消死锁,因此进入死锁后就无法退出。
- 其中一个线程完成其工作后,饥饿将自动结束。
这是一张图片,说明了不同之处。
结论如果线程不足不是您的问题,您仍然可以安全地使用
您可以建立一个规则,说明类可以拥有锁定"this"的代码,或者类中的代码实例化的任何对象。所以,如果不遵循这个模式,这只是个问题。
如果您希望保护自己不受不遵循此模式的代码的影响,那么接受的答案是正确的。但是如果遵循这个模式,那就不是问题了。
锁的优点是效率。如果您有一个简单的"值对象",它保存一个值,该怎么办?它只是一个包装器,它被实例化数百万次。通过只为锁定而创建一个私有同步对象,您基本上已经将对象的大小增加了一倍,分配的数量也增加了一倍。当性能很重要时,这是一个优势。
当您不关心分配数量或内存占用时,出于其他答案中指出的原因,最好避免锁定(这)。
如果可以公开访问该实例,则会出现问题,因为可能有其他请求正在使用同一对象实例。最好使用私有/静态变量。