据MSDN文档所述
public class SomeObject
{
public void SomeOperation()
{
lock(this)
{
//Access instance variables
}
}
}
如果实例可以公开访问,则会出现问题。我想知道为什么?是因为锁定时间比必要时间更长吗?还是有更阴险的原因?
据MSDN文档所述
public class SomeObject
{
public void SomeOperation()
{
lock(this)
{
//Access instance variables
}
}
}
如果实例可以公开访问,则会出现问题。我想知道为什么?是因为锁定时间比必要时间更长吗?还是有更阴险的原因?
this
是不好的形式,因为通常情况下,你无法控制其他人是否正在锁定该对象。this
违反了封装原则,向公众暴露了部分锁定实现。除非已经记录,否则不清楚是否将在this
上获取锁定。即使如此,依靠文档来防止问题也不是最佳选择。lock(this)
实际上修改了作为参数传递的对象,并以某种方式使其只读或无法访问。这是错误的。传递给lock
的对象仅充当键。如果该键已经被锁定,则无法进行锁定;否则,锁定将被允许。lock
语句中使用字符串作为键是不好的,因为它们是不可变的,并且在应用程序的各个部分之间共享/可访问。你应该使用一个私有变量,一个Object
实例即可。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
指针),那么他们也可以尝试锁定同一对象。现在他们可能不知道您正在内部锁定this
,因此这可能会引起问题(可能会导致死锁)。
此外,这也是不良实践,因为它锁定了“太多”东西。
例如,您可能有一个List<int>
成员变量,而您唯一需要锁定的是该成员变量。如果在函数中锁定整个对象,则调用这些函数的其他内容将被阻止等待锁定。如果这些函数不需要访问成员列表,那么您将导致其他代码无缘无故地等待并减慢应用程序的速度。
请查看 MSDN Topic 线程同步(C# 编程指南)。
通常最好避免在公共类型上或对应用程序无法控制的对象实例上进行锁定。例如,如果可以公开访问实例,则 lock(this) 可能存在问题,因为不受您控制的代码也可能在该对象上进行锁定。这可能会创建死锁情况,其中两个或多个线程等待释放相同的对象。锁定公共数据类型(而不是对象)也可能由于同样的原因导致问题。锁定文字字符串尤其危险,因为常规语言运行时(CLR)会对文字字符串进行整理(intern),这意味着对于整个程序,任何给定的字面值字符串都有一个实例,确切地说,在所有运行的应用程序域和所有线程中,同一个对象表示该字面值。结果,放置在应用程序中任何地方具有相同内容的字符串上的锁定都会锁定应用程序中该字符串的所有实例。因此,最好锁定未经整理的私有或受保护成员。一些类专门提供了用于锁定的成员。例如,Array 类提供 SyncRoot。许多集合类型也提供 SyncRoot 成员。
我知道这是一个旧的帖子,但因为人们仍然可以查找并依靠它,所以指出lock(typeof(SomeObject))
比lock(this)
明显更糟糕是很重要的。话虽如此,真诚地向Alan表示敬意,他指出lock(typeof(SomeObject))
是一种不好的实践。
System.Type
的实例是最通用、最粗粒度的对象之一。至少,System.Type的一个实例对于一个AppDomain是全局的,.NET可以在一个AppDomain中运行多个程序。这意味着两个完全不同的应用程序可能会在全局的System.Type实例上互相干扰,甚至可能导致死锁。
因此,lock(this)
不是特别健壮的形式,可能会引起问题,并且对于所有引用原因都应该引起注意。然而,像log4net这样广泛使用、相对受尊敬和表现稳定的代码仍然大量使用lock(this)模式,即使我个人更喜欢看到该模式改变。
但lock(typeof(SomeObject))
会开启一个全新且增强的问题集。
就这些吧。
lock(typeof(SomeObject))
lock(this)
看起来特别逻辑和简洁。这是一种非常粗略的锁定方式,任何其他代码都可以在您的对象上锁定,可能会干扰您的内部代码。采用更细粒度的锁定,并假设 tighter control。lock(this)
的好处是它比lock(typeof(SomeObject))
要好得多。 - Craig Tullis想象你在办公室里有一位熟练的秘书,是部门内的一个共享资源。偶尔你会赶向他们,因为你有一个任务,只希望你的另一个同事还没有占用他们。通常你只需要等待很短的时间。
因为关心就是分享,你的经理决定顾客也可以直接使用这个秘书。但是这有一个副作用:当你正在为这个顾客工作并且你也需要他们执行部分任务时,顾客甚至可能会占用他们。死锁发生了,因为声明不再是层次结构。这完全可以通过一开始不允许顾客声明来避免。
lock(this)
是不好的,正如我们所看到的。外部对象可能锁定该对象,由于您不控制谁在使用该类,任何人都可以锁定它... 这与上述描述的完全相同。解决方法仍然是限制对象的曝光度。但是,如果您有一个 private
、protected
或 internal
类,您已经可以控制谁在锁定您的对象,因为您确信已经自己编写了代码。因此,这里的信息是:不要将其公开为 public
。此外,在类似的情况下使用锁可以避免死锁。
完全相反的是,在应用程序域中锁定共享资源 - 最坏的情况。这就像把你的秘书放在外面,让那里的每个人都可以占用他们。结果是彻底的混乱 - 或者用源代码的术语来说:这是一个坏主意;抛弃它并重新开始。那么我们该怎么做呢?
正如大多数人在这里指出的那样,类型在应用程序域中是共享的。但是我们甚至可以使用更好的东西:字符串。原因是字符串是池化的。换句话说:如果您有两个具有在应用程序域中具有相同内容的字符串,则它们具有完全相同的指针的几率。由于指针用作锁键,您基本上得到了“为未定义的行为做好准备”的同义词。
同样,您不应该锁定WCF对象、HttpContext.Current、Thread.Current、Singletons(通常情况下),等等。避免所有这些的最简单方法是什么呢?private [static] object myLock = new object();
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();
}
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);
}
}
这里是程序在此处锁定的运行。
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上锁定的运行。
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
Random rand = new Random();
时很难发现错误在哪里。我想我看到了,它是重复的余额。 - Seabizkit有一篇关于此的非常好的文章http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects,作者是微软® .NET运行时的性能架构师Rico Mariani。
摘录:
基本问题在于您不拥有类型对象,也不知道谁还会访问它。通常情况下,依赖于锁定一个您没有创建并且不知道谁可能访问的对象是非常糟糕的想法。这样做会引发死锁。最安全的方法是仅锁定私有对象。
https://learn.microsoft.com/en-us/dotnet/standard/threading/managed-threading-best-practices
因此解决方案是向类中添加一个私有对象,例如 lockObject,并在锁语句内部放置代码区域,如下所示:lock (lockObject)
{
...
}
lock(this)
是标准建议;需要注意的是,这样做通常会使外部代码无法在方法调用之间保持与对象关联的锁。这可能是好事或坏事。允许外部代码持有锁的任意时间存在一定的风险,类应该通常被设计为使这种用法不必要,但并不总是有实际的替代方案。举个简单的例子,除非集合实现了自己的ToArray
或ToList
方法... - supercatIEnumerable<T>
扩展方法相反,想要获取集合快照的线程可能只能在锁定所有更改的情况下对其进行枚举。为了做到这一点,它必须访问一个由任何可能会更改集合的代码所获取的锁。如果不公开该锁,则可能无法周期性地执行异步集合快照(例如更新集合浏览用户界面)。 - supercat