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

C# 为什么锁(这个){…}坏了?,c#,multithreading,locking,C#,Multithreading,Locking,报告说 public class SomeObject { public void SomeOperation() { lock(this) { //Access instance variables } } } 如果实例可以公开访问,则会出现问题。我想知道为什么?是不是因为锁的使用时间过长?还是有更阴险的原因呢?因为如果人们可以找到你的对象实例,比如:你的this指针,那么他们也可以尝试锁定同一个对象。现在,他们可能没有意识到您正在内部锁定,因

报告说

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

如果实例可以公开访问,则会出现问题。我想知道为什么?是不是因为锁的使用时间过长?还是有更阴险的原因呢?

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

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


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

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

lock(typeof(SomeObject))

请看一下MSDN主题

通常,最好避免锁定 在公共类型上,或在对象上 无法控制的实例 应用例如,lockthis 如果实例可以 可以公开访问,因为代码 超出您的控制可能会锁定 对象也是。这可能会造成 两个或多个 线程等待 同样的目标。锁定公众 数据类型,与对象相对, 可能会导致同样的问题 原因在文本字符串上锁定是错误的 特别危险,因为 字符串由公共 语言运行库CLR。这意味着 任何一种情况都有一个实例 为整个 程序,完全相同的对象 表示所有运行中的文本 所有线程上的应用程序域。 因此,在字符串上放置一个锁 在任何地方都有相同的内容 应用程序进程锁定所有 中该字符串的实例 应用因此,这是最好的 锁定私有或受保护的成员 那不是实习。一些班级 专门为以下目的提供成员: 锁定。例如,数组类型, 提供同步根目录。许多收藏 类型提供SyncRoot成员作为 嗯


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


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

这里也有一些关于这方面的讨论:

在锁定语句中使用它是一种不好的形式,因为它通常不受您的控制,其他人可能会锁定该对象

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

私有字段通常是更好的选择,因为编译器将强制对其进行访问限制,并且它将封装锁定机制。使用这种方法会向公众公开部分锁定实现,从而违反封装。除非有文件记录,否则也不清楚您是否会获得此锁定。即使如此,依靠文档来防止问题还是次优的

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

这就是为什么在lock语句中使用字符串作为键是不好的,因为它们是不可变的,并且可以在应用程序的各个部分之间共享/访问。你应该使用一个私有变量,一个对象实例就可以了

运行以下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);
    }
}

抱歉,伙计们,但我不同意锁定这可能导致死锁的说法。你混淆了两件事:僵持和饥饿< /p> 在不中断其中一个线程的情况下,无法取消死锁,因此在进入死锁后无法退出 饥饿将在其中一个线程完成其任务后自动结束 这是一幅说明差异的图片

结论
如果线程不足不是您的问题,您仍然可以安全地使用lockthis。您仍然必须记住,当线程(正在使用lock的饥饿线程)在锁定对象的锁中结束时,它最终将在永久饥饿中结束

我知道这是一条古老的线索,但因为人们仍然可以查找并依赖它,所以有必要指出,某个对象的lockTypes比lockthis严重得多。话虽如此,;艾伦指出某种物体的锁型是一种不好的做法,对此艾伦表示衷心的感谢

System.Type的实例是最通用的粗粒度对象之一。至少,System.Type的实例对于AppDomain是全局的,并且.NET可以在AppDomain中运行多个程序。这意味着,如果两个完全不同的应用程序都试图在System.Type的同一全局实例上获得同步锁,则它们可能会相互干扰,甚至可能导致死锁

因此,这不是一个特别健壮的形式,可能会导致问题,并且应该总是因为引用的所有原因而引起关注。尽管我个人更愿意看到这种模式发生变化,但仍有广泛使用的、相对受人尊重的、显然稳定的代码,比如log4net,它广泛地使用了这种模式

但某个物体的锁定类型打开了一个全新的、增强的蠕虫罐


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

以下是一些简单易懂的示例代码:将在LinqPad中工作,参考以下名称空间:System.Net和System.Threading.Tasks

需要记住的是,lockx基本上是语法糖,它所做的是使用Monitor.Enter,然后使用try、catch、finally块调用Monitor.Exit。见:备注部分

或者使用Visual Basic中的C lock语句SyncLock语句, 它将Enter和Exit方法包装在try…finally块中

输出


请注意,Thread12永远不会因为死锁而终止。

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

因为关怀就是分享,所以您的经理决定客户也可以直接使用秘书。但这有一个副作用:当您为该客户工作时,客户甚至可能会提出索赔,并且您还需要他们执行部分任务。发生死锁,因为声明不再是层次结构。这本可以通过不允许客户在第一时间提出索赔来避免

正如我们所看到的,这很糟糕。外部对象可能会锁定该对象,因为您无法控制谁在使用该类,所以任何人都可以锁定该对象。。。这正是上面描述的例子。同样,解决方案是限制对象的曝光。但是,如果您有一个私有的、受保护的或内部的类,您就可以控制谁在锁定您的对象,因为您确信您自己编写了代码。所以这里的信息是:不要将其公开。此外,确保在类似场景中使用锁可以避免死锁

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

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

类似地,您不应该锁定WCF对象、HttpContext.Current、Thread.Current、Singleton等。避免所有这些的最简单方法是什么?私有[静态]对象myLock=新对象

如果您锁定的是共享资源,则锁定此指针可能不好。共享资源可以是静态变量,也可以是计算机上的文件,即在类的所有用户之间共享的资源。原因是这个 每次实例化类时,指针将包含对内存中某个位置的不同引用。因此,在一个类的一次实例中锁定此对象与在另一个类实例中锁定此对象不同

看看这段代码,看看我的意思。将以下代码添加到控制台应用程序中的主程序:

    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();
    }
这是一个程序运行锁定在这个

下面是锁定myLock的程序运行


Microsoft®.NET运行时的性能架构师Rico Mariani写了一篇非常好的文章

摘录:

这里的基本问题是,您不拥有类型对象,而 不知道还有谁可以访问它。总的来说,这是一个非常糟糕的主意 依赖于锁定一个你没有创建的对象,并且不知道还有谁 可能正在访问。这样做会导致僵局。最安全的方法是 仅锁定私有对象


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

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

lock (lockObject)
{
...
}

下面是一个简单得多的例子,说明了为什么锁定这是不好的,当类的使用者也试图锁定对象时,它可能会导致死锁。 下面,三个线程中只有一个线程可以继续,其他两个线程处于死锁状态


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

如果您想保护自己免受不遵循此模式的代码的攻击,那么公认的答案是正确的。但如果遵循这种模式,这就不是问题

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


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

此答案的最后一段不正确。锁定不会以任何方式使对象不可访问或只读。Lockthis不会阻止另一个线程调用或修改此引用的对象。如果正在调用的其他方法也执行Lockthis,则会阻止。我相信这就是他说的重点。请注意,如果在函数中锁定整个对象…@Orion:这更清楚@Herms:是的,但是你不需要使用“this”来实现这个功能,例如,列表中的SyncRoot属性可以达到这个目的,同时明确同步应该在该键上完成。Re:锁定太多:决定锁定什么是一个很好的平衡行为。请注意,获取锁涉及缓存刷新CPU操作,而且成本有点高。换句话说:不要锁定和更新每个单独的整数:最后一段仍然没有意义。如果您只需要限制对列表的访问,那么如果其他函数不访问列表,为什么它们会有锁呢。2同样的Nancy在thread2老化时仍然锁定在thread1中-证明锁定的对象不是只读的。在线程2中,这个对象也被锁定在Name上。3创建具有相同名称的不同对象。4传递到thread3并尝试用Name锁定。大结束,但字符串是不可变的,这意味着任何引用Nancy Drew字符串的对象都在查看内存中的同一个字符串实例。因此,当object1使用标准变量而不是lock锁定在同一个值上时,object2无法获得字符串锁这是标准建议;需要注意的是,这样做通常会使外部代码无法在方法调用之间保持与对象关联的锁。这可能是好事,也可能不是好事。允许外部代码在任意持续时间内持有锁是有一定危险的,类的设计通常应该使这种使用变得不必要,但并不总是有实用的替代方法。举个简单的例子,除非集合实现了自己的ToArray或ToList方法……与` IEnumerable扩展方法'相反,对于希望获取集合快照的线程,唯一的方法可能是在锁定所有更改的同时枚举它。为了做到这一点,它必须能够访问任何可能更改集合的代码所获取的锁。未能公开锁可能会使程序无法定期执行集合的异步快照,例如更新集合浏览用户界面。通常存在一种误解,即锁这实际上修改了
对象作为参数传递,并以某种方式使其只读或不可访问。这是错误的-我相信这些讨论是关于CLR对象中的SyncBlock位的,所以形式上这是右锁修改的对象itself@Esteban,我绝对喜欢你的例子,太棒了。我有个问题要问你。方法名称更改的代码。。结束于:if Monitor.TryEnterperson.Name,10000{…}else Monitor.Exitperson.Name;如果它不是以:if Monitor.TryEnterperson.Name,10000{…Monitor.Exitperson.Name;}不确定这会给这个人带来什么,已经存在的详细答案说明了同样的事情。某个对象的lockTypes实际上比lockthis糟糕得多。那么,lockApplication.Current就更糟糕了,但不管怎样,谁会尝试这些愚蠢的事情呢?lockthis似乎合乎逻辑且简洁,但其他的例子则不然。我不同意lockthis似乎特别合乎逻辑且简洁。这是一个非常粗糙的锁,任何其他代码都可能会锁定您的对象,可能会对您的内部代码造成干扰。采取更细粒度的锁,并采取更严格的控制。lockthis的作用是,它比某个对象的LockTypes好得多。虽然有区别,但它与本文的讨论完全无关。你结论的第一句话完全错了。要清楚的是:我不是在为这个辩护——这种代码是完全错误的。我只是觉得称之为死锁有点滥用。图像链接不再可用你有没有可能再参考一下?实际上,拥有一个私有类并不能阻止这个问题。外部代码可以获取对私有类实例的引用…@Rashack虽然您指出这一点在技术上是正确的+1,但我的观点是您应该控制锁定该实例的人。返回这样的实例会破坏这一点。似乎第二个使用ISLOCK线程的DOWORK并不是说明问题所必需的?您的意思是主线程中的OUTER锁,一个线程只会等待另一个线程完成?这将使平行线失效。。。我觉得我们需要更好的现实世界的例子。@Seabizkit,更新了代码,使其更清晰。并行程序只是为了创建一个新线程并异步运行代码。实际上,第二个线程可以通过多种方式调用,如按钮单击、单独请求等。据我所见,当我用SomeClass的私有实例成员上的锁替换锁this时,我仍然会遇到相同的死锁。此外,如果主类中的锁是在程序的另一个私有实例成员上完成的,则会发生相同的锁。因此,不确定这个答案是否具有误导性和不正确性。看看这里的行为:虽然是真的;-在你的例子中需要注意的是什么,比如你展示了什么是不正确的。当你使用Random rand=new Random时,很难发现什么是错误的;我想我看到了重复的平衡
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
    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
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
lock (lockObject)
{
...
}
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();
        }
            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }