Answers:
this
在锁语句中使用这是一种不好的形式,因为通常是您无法控制的其他人可能会锁定该对象。
为了适当地计划并行操作,应格外小心,以考虑可能的死锁情况,并且锁入口点的数量未知会阻止这种情况。例如,任何引用对象的人都可以在对象设计者/创建者不知道它的情况下锁定它。这增加了多线程解决方案的复杂性,并可能影响其正确性。
私有字段通常是一个更好的选择,因为编译器将强制对其进行访问限制,并且它将封装锁定机制。使用this
通过向公众公开您的锁定实现的一部分来违反封装。也不清楚是否将获得锁定,this
除非已记录。即使那样,依靠文档来防止问题也不是很理想。
最后,存在一个普遍的误解,lock(this)
实际上是修改了作为参数传递的对象,并以某种方式使其变为只读或不可访问。这是错误的。作为参数传递的对象lock
仅用作键。如果该键上已经有锁,则无法进行该锁;否则,将允许锁定。
这就是为什么在lock
语句中使用字符串作为键很不好的原因,因为它们是不可变的,并且可以在应用程序的各个部分之间共享/访问。您应该改用私有变量,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'.
lock(this)
标准建议;重要的是要注意,这样做通常会使外部代码无法在方法调用之间保持与对象关联的锁。 这可能不是好事。允许外部代码在任意持续时间内保持锁定存在一定的危险,通常应设计类以使此类使用变得不必要,但并非总是可行的选择。举一个简单的例子,除非集合实现自己的ToArray
or ToList
方法...
there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false
-我相信这些讨论都是关于CLR对象中的SyncBlock位的,所以正式来说这是正确的-锁定修改后的对象本身
因为如果人们可以得到您的对象实例(即您的this
)指针,那么他们也可以尝试锁定同一对象。现在他们可能不知道您正在this
内部锁定,因此这可能会导致问题(可能是死锁)
除此之外,这也是不好的做法,因为它“锁定”了太多
例如,您的成员变量可能为List<int>
,而您实际上唯一需要锁定的是该成员变量。如果将整个对象锁定在函数中,则调用这些函数的其他事物将被阻止,等待锁定。如果这些函数不需要访问成员列表,则将导致其他代码等待,并毫无理由地降低应用程序的速度。
看看MSDN主题线程同步(C#编程指南)
通常,最好避免锁定公共类型或应用程序无法控制的对象实例。例如,如果可以公开访问该实例,则lock(this)可能会出现问题,因为超出您控制范围的代码也可能会锁定该对象。这可能会导致两个或多个线程等待释放同一对象的死锁情况。与对象相反,锁定公共数据类型可能会导致问题,原因相同。锁定文字字符串特别危险,因为文字字符串由公共语言运行库(CLR)进行了中断。这意味着整个程序的任何给定字符串文字都有一个实例,在所有线程上的所有正在运行的应用程序域中,完全相同的对象代表文字。结果,在应用程序进程中任何位置上具有相同内容的字符串上放置的锁将锁定该字符串在应用程序中的所有实例。因此,最好锁定没有被阻止的私有成员或受保护成员。一些类提供专门用于锁定的成员。例如,数组类型提供SyncRoot。许多集合类型也提供SyncRoot成员。
我知道这是一个旧线程,但是由于人们仍然可以查找并依靠它,因此指出这lock(typeof(SomeObject))
一点明显比差很重要lock(this)
。话说回来; 衷心感谢Alan指出这lock(typeof(SomeObject))
是不好的做法。
的实例System.Type
是其中最通用,最粗糙的对象之一。至少,System.Type的实例是AppDomain的全局实例,.NET可以在AppDomain中运行多个程序。这意味着,如果两个完全不同的程序都试图在同一类型实例上获得同步锁,则它们甚至可能相互造成干扰,甚至产生死锁。
因此lock(this)
并不是特别健壮的形式,可能会引起问题,并且出于所有提及的原因应始终引起人们的注意。然而,有广泛使用的,相对受人尊敬的且似乎稳定的代码(例如log4net)广泛使用了lock(this)模式,尽管我个人更希望看到这种模式更改。
但是却lock(typeof(SomeObject))
打开了全新的蠕虫病毒罐。
物有所值。
...并且完全相同的参数也适用于此构造:
lock(typeof(SomeObject))
lock(this)
似乎是合乎逻辑和简洁的。这是一个非常粗略的锁定,任何其他代码都可能对您的对象进行锁定,从而可能会干扰您的内部代码。采取更多的粒度锁定,并采取更严格的控制。什么lock(this)
确实有去为它是,它是一个很多好过lock(typeof(SomeObject))
。
想象一下,您在办公室有一个熟练的秘书,这是部门中的共享资源。偶尔,您会因为有任务而奔向他们,只是希望您的另一位同事尚未提出要求。通常,您只需要等待一小段时间。
由于关怀共享,您的经理决定客户也可以直接使用秘书。但这有一个副作用:客户在为该客户工作时甚至可能要求他们,并且您还需要他们执行部分任务。发生死锁,因为声明不再是层次结构。可以通过不允许客户首先要求他们来避免这种情况。
lock(this)
如我们所见,这很糟糕。外部对象可能会锁定在该对象上,并且由于您不控制谁在使用该类,因此任何人都可以锁定该对象……这就是上述确切的示例。同样,解决方案是限制对象的曝光。但是,如果你有private
,protected
或internal
类,你可能已经控制了谁是你的对象锁定,因为你相信你自己写的代码。所以这里的信息是:不要将其公开为public
。另外,确保在类似情况下使用锁可以避免死锁。
完全相反的是锁定整个应用程序域中共享的资源-最坏的情况。这就像将您的秘书放到外面,然后允许所有人在那里要求他们。结果是非常混乱-或就源代码而言:这是一个坏主意;扔掉然后重新开始。那么我们该怎么做呢?
正如大多数人在此处指出的那样,类型在应用程序域中共享。但是我们可以使用更好的东西:字符串。原因是字符串被合并。换句话说:如果您在应用程序域中有两个内容相同的字符串,则它们的指针可能完全相同。由于指针用作锁定键,因此您基本上得到的是“为未定义的行为做准备”的同义词。
同样,您不应该锁定WCF对象,HttpContext.Current,Thread.Current,Singletons(通常)等。避免所有这些的最简单方法是? private [static] object myLock = new object();
锁定在这个指针可以是坏的,如果你锁定在一个共享的资源。共享资源可以是静态变量,也可以是计算机上的文件,即该类的所有用户之间共享的资源。原因是,每次实例化类时,this指针将包含对内存中位置的不同引用。因此,锁定了这个在一次类的实例是不是锁定在不同的这一个类的另一个实例。
查看此代码,了解我的意思。将以下代码添加到控制台应用程序的主程序中:
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();
nvm 时出了什么问题我想我会看到它反复出现的Balance
有一篇很好的文章 Microsoft®.NET运行时性能架构师Rico Mariani http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects
摘抄:
这里的基本问题是您不拥有类型对象,并且您不知道还有谁可以访问它。通常,依靠锁定未创建的对象并且不知道还有谁可以访问是一个非常糟糕的主意。这样做会导致死锁。最安全的方法是仅锁定私有对象。
这里也有一些很好的讨论:这是否正确使用了互斥锁?
这是一个更简单的说明(来自此处的问题34),为什么当您的类的使用者也尝试锁定该对象时,lock(this)不好并可能导致死锁。在下面,三个线程中只有一个可以继续进行,其他两个则处于死锁状态。
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(带有超时)而不是锁:
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
SomeClass
,我仍然会遇到相同的死锁。同样,如果主类中的锁定是在Program的另一个私有实例成员上完成的,则会发生相同的锁定。因此,不确定此答案是否会引起误解和错误。在此处查看该行为:dotnetfiddle.net/DMrU5h
抱歉,伙计们,但我不同意将锁锁定可能导致死锁的说法。您会混淆两件事:僵局和饥饿。
这是说明差异的图片。
结论如果线程饥饿对您来说不是问题,您
仍然可以安全使用lock(this)
。您仍然需要记住,当线程(正在使线程lock(this)
处于饥饿状态)在锁定对象的锁中结束时,它最终将以永恒的饥饿而结束;)
lock(this)
-这种代码简直是错误的。我只是认为称之为死锁有点辱骂。
请参考以下链接,该链接解释了为什么锁(此)不是一个好主意。
http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx
因此解决方案是将一个私有对象(例如lockObject)添加到该类,并将代码区域放置在lock语句内,如下所示:
lock (lockObject)
{
...
}
以下是一些更易于遵循(IMO)的示例代码:(将在LinqPad中工作,引用以下命名空间:System.Net和System.Threading.Tasks)
要记住的一点是lock(x)基本上是语法糖,它的作用是使用Monitor.Enter,然后使用try,catch,finally块调用Monitor.Exit。请参阅:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.enter(“备注”部分)
或使用C#lock语句(Visual Basic中为SyncLock语句),该语句将Enter和Exit方法包装在try ... finally块中。
void Main()
{
//demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
ClassTest test = new ClassTest();
lock(test) //locking on the instance of ClassTest
{
Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Parallel.Invoke(new Action[]
{
() => {
//this is there to just use up the current main thread.
Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
},
//none of these will enter the lock section.
() => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
() => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
});
}
}
public class ClassTest
{
public void DoWorkUsingThisLock(int i)
{
Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
{
Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
}
Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
}
public void DoWorkUsingMonitor(int i)
{
Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
if (Monitor.TryEnter(this))
{
Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
Monitor.Exit(this);
}
else
{
Console.WriteLine($"Skipped lock section! {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
}
Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine();
}
}
输出量
CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section! 2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13
请注意,线程#12永远不会因为其死锁而终止。
DoWorkUsingThisLock
没有必要使用第二个线程来说明问题?