为什么锁定System.Object的实例与锁定特定实例类型不同



我一直在学习线程上的锁定,但我没有找到解释为什么要创建一个典型的System.Object,锁定它并在锁定过程中执行所需的任何操作来提供线程安全?

示例

object obj = new object()
lock (obj) {
  //code here
}

起初,我认为它只是用作示例中的占位符,意味着要与您正在处理的类型交换。但我发现Dennis Phillips指出的例子,似乎与实际使用Object实例没有什么不同。

那么,举一个需要更新私有字典的例子,锁定System.Object的实例与实际锁定字典(我知道在这种情况下锁定字典可能会出现同步问题)相比,能提供线程安全吗?如果字典是公开的呢?

//what if this was public?
private Dictionary<string, string> someDict = new Dictionary<string, string>();
var obj = new Object();
lock (obj) {
  //do something with the dictionary
}

lock本身对Dictionary<TKey, TValue>类型没有任何安全性。lock本质上是

对于给定对象(objInstance),每次使用lock(objInstance)时,只有一个线程会出现在lock语句的主体中

如果每次使用给定的CCD_ 6实例都发生在CCD_。如果这些lock中的每一个都使用相同的对象,那么您就知道一次只有一个线程在访问/修改字典。这对于防止多个线程同时对其进行读写以及损坏其内部状态至关重要。

不过,这种方法有一个巨大的问题:您必须确保每次使用字典都发生在锁内,并且使用相同的对象。如果你忘记了一个,那么你已经创建了一个潜在的竞争条件,编译器将不会发出警告,而且这个错误可能会在一段时间内仍未被发现。

在第二个示例中,您展示了使用本地对象实例(var表示方法local)作为对象字段的锁参数。这几乎可以肯定是错误的做法。当地人只会在这种方法的一生中活着。因此,对该方法的2个调用将在不同的局部使用锁,因此所有方法都能够同时进入锁。

锁定共享数据本身曾经是一种常见的做法:

private Dictionary<string, string> someDict = new Dictionary<string, string>();
lock (someDict ) 
{
  //do something with the dictionary
}

但(有些理论上的)反对意见是,在您控制之外的其他代码也可能锁定someDict,然后您可能会出现死锁。

因此,建议使用一个(非常)私有的对象,以与数据一一对应的方式声明,作为锁的替代。只要所有访问字典的代码都锁定在obj上,就可以保证踏面安全。

// the following 2 lines belong together!!
private Dictionary<string, string> someDict = new Dictionary<string, string>();
private object obj = new Object();

// multiple code segments like this
lock (obj) 
{
  //do something with the dictionary
}

因此,obj的目的是充当字典的代理,因为它的类型无关紧要,所以我们使用最简单的类型System.Object

如果字典是公开的呢?

然后所有的赌注都被取消了,任何代码都可以访问Dictionary,并且包含类之外的代码甚至无法锁定guard对象。在你开始寻找解决方案之前,这根本不是一个可持续的模式。使用ConcurrentDictionary或保持一个普通的私有。

用于锁定的对象与锁定期间修改的对象无关。它可以是任何东西,但应该是私有的,没有字符串,因为公共对象可以在外部修改,字符串可能会被两个锁错误地使用。

据我所知,使用通用object只是为了锁定一些东西(作为内部可锁定的对象)。为了更好地解释这一点;假设一个类中有两个方法,它们都访问Dictionary,但可能在不同的线程上运行。为了防止两种方法同时修改Dictionary(并可能导致死锁),可以锁定一些对象来控制流。下面的例子可以更好地说明这一点:

private readonly object mLock = new object();
public void FirstMethod()
{
    while (/* Running some operations */)
    {
        // Get the lock
        lock (mLock)
        {
            // Add to the dictionary
            mSomeDictionary.Add("Key", "Value");
        }
    }
}
public void SecondMethod()
{
    while (/* Running some operation */)
    {
        // Get the lock
        lock (mLock)
        {
            // Remove from dictionary
            mSomeDictionary.Remove("Key");
        }
    }
}

在同一对象上的两个方法中使用lock(...)语句可以防止这两个方法同时访问资源。

锁定对象的重要规则是:

  1. 它必须是一个只对需要锁定它的代码可见的对象。这避免了其他代码也锁定它。

    1. 这就排除了可以被插入的字符串和Type对象
    2. 在大多数情况下,这排除了this,并且异常太少,在利用方面提供的很少,所以不要使用this
    3. 还要注意的是,框架内部的一些情况锁定了Typethis,所以虽然"只要没有其他人做就可以"是真的,但已经太晚了
  2. 它必须是静态的以保护静态静态操作,也可以是实例以保护实例操作(包括静态中的实例内部的操作)。

  3. 您不希望锁定值类型。如果你真的也想,你可以锁定一个特定的盒子,但除了证明它在技术上是可能的之外,我想不出这会有什么收获——这仍然会导致代码不太清楚什么锁定了什么。

  4. 你不想锁定一个在锁定期间可能会更改的字段,因为你将不再锁定你看起来已经锁定的字段(这几乎是合理的,它有实际的用途,但代码在第一次读取时看起来做的和它真正做的之间会有一个阻抗,这从来都不是好的)。

  5. 必须使用同一对象锁定所有可能相互冲突的操作。

  6. 虽然使用过宽的锁可以获得正确性,但使用更精细的锁可以得到更好的性能。例如,如果你有一个保护6个操作的锁,并且意识到其中2个操作不会干扰其他4个操作,所以你改为拥有2个锁对象,那么你可以通过更好的一致性(或者如果你在分析中出错,则崩溃并烧毁!)

第一点排除了锁定任何可见或可能可见的内容(例如,受保护成员或公共成员返回的私有实例应被视为公共实例,委托捕获的任何内容都可能被转移到其他地方,等等)。

最后两点可能意味着,正如你所说,没有明显的"你正在处理的类型",因为锁不保护对象,也不保护对对象执行的操作,你可能有多个对象受到影响,或者同一个对象受到多组必须锁定的操作的影响。

因此,拥有一个纯粹为锁定而存在的对象可能是一种很好的做法。由于它不做任何其他事情,所以它不会与其他语义混淆,也不会在你意想不到的时候被重写。由于它不做任何其他事情,它可能是.NET中存在的最轻的引用类型;System.Object

就我个人而言,我确实更喜欢锁定一个与操作相关的对象,因为它确实符合"你正在处理的类型"的要求,而且其他问题都不适用,因为在我看来,这是一种自我记录,但对其他人来说,做错事的风险会抵消这一好处。

最新更新