为什么锁(这个){…}坏?

MSDN文档表示

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

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

179532 次浏览

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

除此之外,这也是一种糟糕的做法,因为它锁定了“太多”

例如,您可能有一个成员变量List<int>,实际上您唯一需要锁定的就是这个成员变量。如果你在你的函数中锁定了整个对象,那么其他调用这些函数的东西将被阻塞等待锁定。如果这些函数不需要访问成员列表,则会导致其他代码等待,并毫无理由地降低应用程序的速度。

...同样的论点也适用于这个结构:

lock(typeof(SomeObject))

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

一般情况下,最好避免锁定 在公共类型或对象上 超出你控制范围的情况 应用程序。例如,lock(this) 如果实例可以,那么它也会有问题吗 被公开访问,因为代码 超出你的控制可能锁定 对象也是。这可以创建 两个或更多的死锁情况 线程等待 同一对象< / b >。锁定公众 数据类型,而不是对象, 会不会引起同样的问题 的原因。锁定字面值字符串是 尤其危险的是 字符串是由common(通用)来存储的 语言运行库。这意味着 有一个任何的例子 给定整个的字符串字面量 程序,完全相同的对象 表示所有运行中的文字 应用程序域,在所有线程上。 结果,一个锁被放置在字符串上 相同的内容 应用程序进程锁定所有 对象中该字符串的实例 应用程序。因此,这是最好的 锁定私有或受保护的成员 这是不可避免的。一些类 为以下人员提供成员 锁定。数组类型,例如, 提供SyncRoot。许多收集 类型提供SyncRoot成员为 好。< / p >

因为任何可以看到类实例的代码块也可以锁定该引用。您希望隐藏(封装)锁定对象,以便只有需要引用它的代码才能引用它。关键字this指向当前类实例,因此任何数量的东西都可以引用它,并可以使用它来进行线程同步。

需要明确的是,这很糟糕,因为其他一些代码块可能会使用类实例来锁定,并且可能会阻止您的代码获得及时的锁定,或者可能会产生其他线程同步问题。最好的情况是:没有其他方法使用对您的类的引用来锁定。中间情况:某些东西使用对你的类的引用来锁,这导致了性能问题。最坏的情况:某些东西使用你的类的引用来进行锁,这会导致非常糟糕、非常微妙、非常难以调试的问题。

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

在锁语句中使用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'.

抱歉,伙计们,我不同意锁定这个可能会导致僵局的说法。你混淆了两件事:僵局和饥饿。

  • 如果不中断其中一个线程,就无法取消死锁,因此进入死锁后就无法退出
  • 饥饿将在其中一个线程完成其工作后自动结束

在这里是一张说明了区别的图片。

< p > # EYZ0 < br > 如果线程饥饿对您来说不是问题,您仍然可以安全地使用lock(this)。你仍然要记住,当线程(使用lock(this)挨饿的线程)以锁定对象的锁结束时,它将最终以永远饥饿结束;)

我知道这是一个旧的线程,但因为人们仍然可以查找并依赖它,所以有必要指出lock(typeof(SomeObject))明显比lock(this)差。话虽如此;艾伦指出lock(typeof(SomeObject))是一种糟糕的做法。

System.Type的实例是最通用的粗粒度对象之一。至少是System的一个实例。Type对于AppDomain是全局的,. net可以在一个AppDomain中运行多个程序。这意味着,如果两个完全不同的应用程序都试图在System.Type的同一个全局实例上获得同步锁,那么它们可能会对彼此造成潜在的干扰,甚至产生死锁。

因此,lock(this)并不是特别健壮的形式,可能会导致问题,并且应该总是因为所引用的所有原因而引起注意。然而,像log4net这样广泛使用、相对受人尊敬且明显稳定的代码广泛使用锁(此)模式,尽管我个人更希望看到这种模式发生变化。

但是lock(typeof(SomeObject))打开了一个全新的增强的蠕虫罐头。

不管怎样。

如果可以公开访问实例,就会出现问题,因为可能有其他请求正在使用相同的对象实例。最好使用私有/静态变量。

下面是一些更容易遵循的示例代码(IMO):(将在LinqPad中工作,引用以下名称空间:System. System.)Net和System.Threading.Tasks)

需要记住的一点是,lock(x)基本上是语法糖,它所做的就是使用Monitor。输入,然后使用try、catch和finally块调用Monitor.Exit。参见:https://learn.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter(备注部分)

或使用c#锁语句(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永远不会因为死锁而结束。

想象一下,你的办公室有一位技术娴熟的秘书,这是部门的共享资源。偶尔,你会因为有任务而冲向他们,只希望你的另一个同事还没有认领他们。通常你只需要等很短的一段时间。

因为关心就是分享,你的经理决定顾客也可以直接使用秘书。但这有一个副作用:当您为该客户工作时,客户甚至可能会认领它们,并且您还需要他们执行部分任务。死锁发生,因为声明不再是层次结构。如果从一开始就不允许客户索赔,这完全可以避免。

正如我们所看到的,lock(this)很糟糕。外部对象可能会锁定该对象,由于您无法控制谁在使用该类,因此任何人都可以锁定它……这就是上面描述的例子。同样,解决方案是限制物体的曝光。然而,如果你有一个privateprotectedinternal类,你会已经可以控制谁锁定你的对象,因为你确定你自己写了你的代码。所以这里的信息是:不要将它暴露为public。另外,确保在类似的场景中使用锁可以避免死锁。

与此完全相反的是锁定整个应用领域共享的资源——这是最坏的情况。这就像把你的秘书放在外面,让外面的人去认领一样。结果是彻底的混乱——或者就源代码而言:这是一个坏主意;把它扔掉,重新开始。我们怎么做呢?

正如这里大多数人指出的那样,类型在应用领域是共享的。但是我们可以使用更好的东西:字符串。原因是字符串是汇集。换句话说,如果你有两个字符串在一个应用域中有相同的内容,它们有可能有完全相同的指针。由于指针被用作锁键,基本上你得到的是“为未定义的行为做好准备”的同义词。

类似地,你不应该锁定WCF对象HttpContext。目前,线程。当前,单例(一般)等。避免这一切的最简单方法是什么?# EYZ0

如果您锁定的是共享资源指针,则锁定指针可以是指针。共享资源可以是一个静态变量,也可以是计算机上的一个文件——即在类的所有用户之间共享的一些东西。原因是,每次实例化类时,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

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

摘录:

这里的基本问题是你不拥有类型对象,而你 不知道还有谁能拿到。总的来说,这是一个非常糟糕的主意 依赖于锁定一个不是你创建的对象,也不知道还有谁 可能是存取。这样做会导致僵局。最安全的方法是

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

https://learn.microsoft.com/en-us/dotnet/standard/threading/managed-threading-best-practices

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

lock (lockObject)
{
...
}
这里有一个更简单的例子(来自这里是34题),为什么锁(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(带超时)而不是lock:

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
if (lockWasTaken)
{
doAction();
}
else
{
throw new Exception("Could not get lock");
}

< a href = " https://blogs.appbeat。Io /post/c-how-to-lock-without-deadlocks" rel="nofollow noreferrer">https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks .

你可以建立一个规则,规定一个类可以有锁定'this'或类中代码实例化的任何对象的代码。所以只有不遵循这个模式才会有问题。

如果您想保护自己不受不遵循此模式的代码的影响,那么接受的答案是正确的。但如果遵循这个模式,那就不是问题。

锁的优点是效率高。如果你有一个简单的“值对象”,它只包含一个值。它只是一个包装器,它被实例化了数百万次。通过要求仅为锁定而创建一个私有同步对象,基本上使对象的大小增加了一倍,分配的数量也增加了一倍。当性能很重要时,这是一个优势。

当您不关心分配数量或内存占用时,出于其他答案中指出的原因,避免锁定(这)是可取的。

在这里是不建议使用的原因。

< p > # EYZ0
考虑下面的代码片段:

object foo = new Object();
object bar = foo;


lock(foo)
{
lock(bar){}
}

这里,foo和bar引用的是导致死锁的同一个对象实例。这是现实中可能发生的事情的简化版本。

< p > # EYZ0
为了根据下面的代码片段更详细地解释它,假设您编写了一个类(在本例中为SomeClass),并且类的使用者(名为"John"的编码器)希望获得类实例(在本例中为someObject)上的锁。他遇到死锁是因为他在实例someObject上获得了一个锁,在这个锁中他调用了该实例(SomeMethod())的一个方法,该方法在内部获得了同一个实例上的锁
  • 我本可以使用或不使用Task/Thread编写下面的示例,死锁的要点仍然保持不变。

  • 为了防止主线程结束而子线程还在运行的奇怪情况,我使用了.Wait()。然而,在长时间运行的任务或代码段执行更频繁的情况下,您肯定会看到相同的行为。

  • 虽然John应用了使用类实例作为锁对象的错误做法,但是我们(作为类库SomeClass的开发人员)应该通过在我们的类中不使用this作为锁对象来防止这种情况。

  • 相反,我们应该声明一个简单的私有字段并使用它作为我们的锁定对象。

     using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    
    class SomeClass
    {
    public void SomeMethod()
    {
    //NOTE: Locks over an object that is already locked by the caller.
    //      Hence, the following code-block never executes.
    lock (this)
    {
    Console.WriteLine("Hi");
    }
    }
    }
    
    
    public class Program
    {
    public static void Main()
    {
    SomeClass o = new SomeClass();
    
    
    lock (o)
    {
    Task.Run(() => o.SomeMethod()).Wait();
    }
    
    
    Console.WriteLine("Finish");
    }
    }