首页 文章

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

提问于
浏览
429

MSDN documentation

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

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

15 回答

  • 41

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

    摘抄:

    这里的基本问题是您没有类型对象,并且您不知道还有谁可以访问它 . 一般来说,依靠锁定一个你没有创建的对象并且不知道还有谁可能访问它是一个非常糟糕的主意 . 这样做会引发僵局 . 最安全的方法是仅锁定私有对象 .

  • 2

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

  • 32

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

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

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

    最后,有一种常见的误解,即 lock(this) 实际上修改了作为参数传递的对象,并以某种方式使其成为只读或不可访问 . 这是 false . 作为参数传递给 lock 的对象仅用作 key . 如果已经锁定了该锁,则无法进行锁定;否则,允许锁定 .

    这就是使用字符串作为 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

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

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

    例如,您可能拥有 List<int> 的成员变量,并且您实际需要锁定的唯一内容是该成员变量 . 如果在函数中锁定整个对象,则会阻止调用这些函数的其他东西等待锁定 . 如果这些功能不会导致其他代码等待并且根本没有任何理由减慢您的应用程序 .

  • 1

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

    通常,最好避免锁定公共类型或超出应用程序控制范围的对象实例 . 例如,如果可以公开访问实例,则lock(this)可能会出现问题,因为超出您控制范围的代码也可能会锁定该对象 . 这可能会导致两个或多个线程等待释放同一对象的死锁情况 . 锁定公共数据类型而不是对象可能会出于同样的原因导致问题 . 锁定文字字符串特别危险,因为文字字符串由公共语言运行库(CLR)实现 . 这意味着整个程序的任何给定字符串文字都有一个实例,完全相同的对象代表所有线程上所有正在运行的应用程序域中的文字 . 因此,在应用程序进程中的任何位置放置在具有相同内容的字符串上的锁会锁定应用程序中该字符串的所有实例 . 因此,最好锁定未实习的私人或受保护成员 . 有些类专门为锁定提供成员 . 例如,Array类型提供SyncRoot . 许多集合类型也提供SyncRoot成员 .

  • 6

    我知道这是一个老线程,但因为人们仍然可以查看并依赖它,所以指出 lock(typeof(SomeObject)) 明显比 lock(this) 差 . 话说回来;对Alan的诚挚称赞,指出 lock(typeof(SomeObject)) 是不好的做法 .

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

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

    但是 lock(typeof(SomeObject)) 开辟了一个全新的,增强的蠕虫病毒 .

    物有所值 .

  • 4

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

    lock(typeof(SomeObject))
    
  • 59

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

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

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

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

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

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

  • 25

    如果要锁定共享资源,则锁定此指针可能会很糟糕 . 共享资源可以是静态变量或计算机上的文件 - 即在类的所有用户之间共享的内容 . 原因是每次实例化类时,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
    
  • 464

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

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

  • 3

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

    请参考以下链接解释为什么锁(这个)不是一个好主意 .

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

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

    lock (lockObject)
    {
    ...
    }
    
  • 1

    这是一个更简单的例子(取自Question 34 here)为什么锁(这个)很糟糕,当你的类的消费者也试图锁定对象时可能会导致死锁 . 下面,只有三个线程中的一个可以继续,另外两个是死锁的 .

    类SomeClass
    {
    public void SomeMethod(int id)
    {
    锁(本)
    {
    而(真)
    {
    Console.WriteLine(“SomeClass.SomeMethod#”id);
    }
    }
    }
    }

    课程
    {
    static void Main(string [] args)
    {
    SomeClass o = new SomeClass();

    锁(O)
    {
    for(int threadId = 0; threadId <3; threadId)
    {
    线程t =新线程(()=> {
    o.SomeMethod(的threadId);
    });
    t.Start();
    }

    Console.WriteLine();
    }

    为了解决这个问题,这个人使用Thread.TryMonitor(带超时)而不是锁定:

    Monitor.TryEnter(temp,millisecondsTimeout,ref lockWasTaken);
    if(lockWasTaken)
    {
    doAction();
    }
    其他
    {
    抛出新的异常(“无法获取锁定”);
    }

    https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks

  • 0

    对不起的家伙,但我不能同意锁定这可能导致死锁的论点 . 你混淆了两件事:僵局和饥饿 .

    • 你不能在不中断其中一个线程的情况下取消死锁,所以在你陷入僵局之后你无法脱身

    • 饥饿将在其中一个线程完成其工作后自动结束

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

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

  • 1

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

相关问题