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

时间:2008-10-30 19:19:22

标签: c# multithreading locking

MSDN documentation

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

是“如果可以公开访问实例的问题”。我想知道为什么?是因为锁定的持有时间超过了必要的时间吗?还是有一些更阴险的原因?

16 个答案:

答案 0 :(得分:479)

在锁定语句中使用this是不好的形式,因为通常无法控制其他人可能锁定该对象。

为了正确规划并行操作,应特别注意考虑可能的死锁情况,并且具有未知数量的锁定入口点会妨碍这一点。例如,任何具有对象引用的人都可以在没有对象设计者/创建者知道的情况下锁定它。这增加了多线程解决方案的复杂性,并可能影响其正确性。

私有字段通常是更好的选项,因为编译器将对其强制实施访问限制,并且它将封装锁定机制。使用this通过将部分锁定实现暴露给公众来违反封装。除非已记录在案,否则您还不清楚是否会获得this上的锁定。即使这样,依靠文档来防止问题也是次优的。

最后,有一种常见的误解,即lock(this)实际上修改了作为参数传递的对象,并以某种方式使其成为只读或不可访问。这是 false 。作为参数传递给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'.

答案 1 :(得分:62)

因为如果人们可以访问你的对象实例(即:你的this)指针,那么他们也可以尝试锁定同一个对象。现在他们可能没有意识到你在内部锁定this,所以这可能会导致问题(可能是死锁)

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

例如,您可能有List<int>的成员变量,并且您实际需要锁定的唯一内容是该成员变量。如果在函数中锁定整个对象,则会阻止调用这些函数的其他东西等待锁定。如果这些函数不需要访问成员列表,那么您将无缘无故地等待其他代码并使应用程序变慢。

答案 2 :(得分:42)

查看MSDN主题Thread Synchronization (C# Programming Guide)

  

通常,最好避免锁定   在公共类型或对象上   超出你控制范围的实例   应用。例如,锁定(这个)   如果实例可以,可能会有问题   公开访问,因为代码   超出你的控制可能会锁定   对象也是。 这可能会造成   死锁情况两个或两个以上   线程等待释放   同一个对象。锁定公众   数据类型,而不是对象,   会导致同样的问题   原因。锁定文字字符串是   特别危险,因为文字   字符串由公共字符串实现   语言运行时(CLR)。这意味着   有任何一个实例   给出整个字符串文字   程序,完全相同的对象   表示所有运行中的文字   应用程序域,在所有线程上。   结果,一个锁放在一个字符串上   在任何地方都有相同的内容   应用程序进程锁定所有   该字符串的实例   应用。因此,这是最好的   锁定私人或受保护的成员   那不是实习生。一些课程   专门为会员提供   锁定。例如,Array类型   提供SyncRoot。很多收藏   types提供SyncRoot成员   好。

答案 3 :(得分:32)

我知道这是一个旧线程,但由于人们仍然可以查看并依赖它,因此指出lock(typeof(SomeObject))明显比lock(this)更糟糕似乎很重要。话说回来;对Alan的真诚赞誉,指出lock(typeof(SomeObject))是不好的做法。

System.Type的实例是最通用的粗粒度对象之一。至少,System.Type的实例是AppDomain的全局实例,.NET可以在AppDomain中运行多个程序。这意味着如果两个完全不同的程序都试图在同一类型的实例上获得同步锁定,那么即使在创建死锁的程度上,它们也可能会相互干扰。

所以lock(this)不是特别健壮的形式,可能会引起问题,并且应该总是因为引用的所有原因引起人们的注意。然而,广泛使用,相对备受尊重且表面稳定的代码如log4net广泛使用锁(this)模式,即使我个人更喜欢看到模式改变。

但是lock(typeof(SomeObject))开辟了一整套新的和增强的蠕虫病毒。

为了它的价值。

答案 4 :(得分:25)

...并且完全相同的参数也适用于此构造:

lock(typeof(SomeObject))

答案 5 :(得分:6)

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

由于照顾是分享,您的经理决定客户也可以直接使用秘书。但这有一个副作用:客户甚至可能在为这个客户工作时声称他们,并且您还需要他们执行部分任务。发生死锁,因为声明不再是层次结构。通过不允许客户首先声明它们,可以一起避免这种情况。

正如我们所见,

lock(this)很糟糕。外部对象可能会锁定对象,因为您无法控制谁在使用该类,所以任何人都可以锁定它...这是上面描述的确切示例。同样,解决方案是限制物体的暴露。但是,如果您有privateprotectedinternal类,则可以控制谁锁定您的对象,因为您确定自己我自己写了你的代码。所以这里的消息是:不要将其公开为public。此外,确保在类似方案中使用锁可以避免死锁。

与此完全相反的是锁定整个应用领域共享的资源 - 最糟糕的情况。这就像把你的秘书放在外面,允许每个人在那里声称他们。结果是完全混乱 - 或者在源代码方面:这是一个坏主意;扔掉它然后重新开始。那我们该怎么做呢?

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

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

答案 6 :(得分:4)

如果要锁定共享资源,则锁定指针可能错误。共享资源可以是静态变量或计算机上的文件 - 即在类的所有用户之间共享的内容。原因是每次实例化类时,this指针将包含对内存中某个位置的不同引用。因此,在类的一次实例中锁定 this 与在另一个类实例中锁定 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);
    }
}

这是一个程序锁定 this

   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

答案 7 :(得分:3)

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

摘录:

  

这里的基本问题是你不拥有类型对象和你   不知道还有谁可以访问它。总的来说,这是一个非常糟糕的主意   依靠锁定一个你没有创建的对象而不知道还有谁   可能正在访问。这样做会引发僵局。最安全的方法是   只锁定私人物品。

答案 8 :(得分:2)

这里也有一些很好的讨论:Is this the proper use of a mutex?

答案 9 :(得分:2)

这是一个更简单的说明(摘自Question 34 here),为什么当类的使用者也尝试锁定该对象时,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

答案 10 :(得分:1)

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

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

答案 11 :(得分:1)

以下是一些更易于遵循的示例代码(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);
    }
}

答案 12 :(得分:1)

请参阅以下链接,该链接解释了锁定(这个)不是一个好主意的原因。

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

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

lock (lockObject)
{
...
}

答案 13 :(得分:0)

很抱歉,但是我不能同意锁定这个可能导致死锁的论点。你混淆了两件事:僵局和饥饿。

  • 你不能在不中断其中一个线程的情况下取消死锁,所以在你陷入死锁之后你无法脱身
  • 饥饿将在其中一个主题完成其作业后自动结束

Here是一张说明差异的图片。

<强>结论
如果线程饥饿不是您的问题,您仍然可以安全地使用lock(this)。你仍然需要记住,当使用lock(this)的饥饿线程的线程以锁定对象的锁定结束时,它最终会以永恒的饥饿结束;)

答案 14 :(得分:0)

您可以建立一条规则,该规则说一个类可以具有锁定在“此”或该类实例化的任何对象上的代码。因此,如果不遵循模式,这只是一个问题。

如果您想保护自己免受不遵循此模式的代码的侵害,那么可接受的答案是正确的。但是,如果遵循这种模式,那不是问题。

锁定(this)的优点是效率。如果您有一个包含单个值的简单“值对象”,该怎么办。它只是一个包装,它实例化了数百万次。通过要求创建仅用于锁定的私有同步对象,您基本上将对象的大小增加了一倍,分配的数量也增加了一倍。当性能很重要时,这是一个优势。

当您不关心分配数量或内存占用时,出于其他答案中指出的原因,最好避免使用锁。

答案 15 :(得分:-1)

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