【问题标题】:Creating a mutiple syncLock variable for an instance为实例创建多个 syncLock 变量
【发布时间】:2011-07-22 07:54:55
【问题描述】:

我有两个使用延迟加载支持字段的内部属性,并在多线程应用程序中使用,因此我根据this MSDN article 实现了双重检查锁定方案

现在,首先假设这是一个合适的模式,所有示例都显示为实例创建单个锁对象。如果我的两个属性是相互独立的,那么为每个属性创建一个锁实例不是更高效吗?

我突然想到,为了避免死锁或竞争条件,可能只有一个。没有想到一个明显的情况,但我相信有人可以给我看一个......(显然,我对多线程代码不是很有经验)

private List<SomeObject1> _someProperty1;
private List<SomeObject2> _someProperty2;
private readonly _syncLockSomeProperty1 = new Object();
private readonly _syncLockSomeProperty2 = new Object();

internal List<SomeObject1> SomeProperty1
{
  get
  {
    if (_someProperty1== null)
    {
      lock (_syncLockSomeProperty1)
      {
        if (_someProperty1 == null)
        {
          _someProperty1 = new List<SomeObject1>();
        }
      }
    }
    return _someProperty1;
  }

  set
  {
    _someProperty1 = value;
  }
}

internal List<SomeObject2> SomeProperty2
{
  get
  {
    if (_someProperty2 == null)
    {
      lock (_syncLockSomeProperty2)
      {
        if (_someProperty2 == null)
        {
          _someProperty2 = new List<SomeObject2>();
        }
      }
    }
    return _someProperty2;
  }

  set
  {
    _someProperty2 = value;
  }
}

【问题讨论】:

  • 属性设置器是一种糟糕的代码气味。这里不止一种方式。
  • 最后,我查看了我继承的代码并对其进行了重构,使其成为线程安全的,无需任何锁定。那么,到底最好的解决方案...

标签: c# object locking synchronized


【解决方案1】:

是的,如果它们彼此独立,这确实会更有效,因为访问一个不会阻止访问另一个。如果这种独立性被证明是错误的,您也会担心陷入僵局的风险。

问题是,假设 _someProperty1 = new List&lt;SomeObject1&gt;(); 不是分配给 _someProperty1 的真实代码(几乎不值得延迟加载,是吗?),那么问题是:填充 SomeProperty1 的代码是否可以调用通过任何代码路径填充 SomeProperty2,反之亦然,无论多么奇怪?

即使一个可以调用另一个,也不会出现死锁,但是如果它们都可以互相调用(或者1调用2,2调用3,3调用1,以此类推),那么死锁可以肯定会发生。

作为一项规则,我会从宽锁开始(一个锁用于所有锁定的任务),然后根据需要将锁缩小以进行优化。例如,如果您有 20 个需要锁定的方法,那么判断安全性可能会更难(而且,您开始仅使用锁定对象填充内存)。

请注意,您的代码也存在两个问题:

首先,您不要锁定您的二传手。可能这很好(你只希望你的锁防止对加载方法的多次大量调用,并且实际上并不关心setget 之间是否存在覆盖),这可能是一场灾难.

其次,根据运行它的 CPU,在编写时仔细检查可能会出现读/写重新排序问题,因此您应该有一个 volatile 字段,或者调用内存屏障。见http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspx

编辑:

它是否真的需要也值得考虑。

考虑操作本身应该是线程安全的:

  1. 做一堆东西就完成了。
  2. 根据那一堆东西创建一个对象。
  3. 将该对象分配给局部变量。

1 和 2 只会发生在一个线程上,而 3 是原子的。因此,加锁的好处是:

  1. 如果执行上述第 1 步和/或第 2 步有它们自己的线程问题,并且没有通过它们自己的锁来保护它们,那么锁定是 100% 必要的。

  2. 如果对在第 1 步和第 2 步中获得的值采取行动会造成灾难性后果,然后在重复第 1 步和第 2 步的情况下这样做,那么锁定是 100% 必要的。

  3. 锁定将防止多次执行 1 和 2 的浪费。

因此,如果我们可以排除案例 1 和 2 的问题(需要进行一些分析,但通常是可能的),那么我们只需要担心案例 3 中的浪费。现在,也许这是一个很大的担忧。但是,如果它很少出现,并且在出现时也没有那么浪费,那么不锁定的收益将超过锁定的收益。

如果有疑问,锁定可能是更安全的方法,但可能只是忍受偶尔浪费的操作会更好。

【讨论】:

    【解决方案2】:

    如果这两个属性(或者更具体地说是它们的初始化程序)彼此独立,就像您提供的示例代码一样,拥有两个不同的锁对象是有意义的。但是,当初始化很少发生时,影响可以忽略不计。

    请注意,您还应该保护 setter 的代码。 lock 语句强加了所谓的内存屏障,这对于防止竞态条件尤其是在多 CPU 和/或多核系统上是必不可少的。

    【讨论】:

    • 简单的setter不需要锁;引用分配是原子的。只有大于原生字长的赋值(例如大值类型)才是非原子赋值。
    • @Adam Robinson:不。原子性与并发访问和内存屏障无关。阅读:albahari.com/threading/part4.aspx
    • @Ondrej:简单地在 setter 中分配一个引用不需要内存屏障;没有潜在的竞争条件,因为唯一发生的事情是单个原子操作。
    • @Adam:这不仅仅是竞争条件的问题,还有代码优化。
    • @Ondrej:而且,正如我所说,唯一发生的操作是单个引用分配。此处无法进行重新排序(或任何其他优化)。
    【解决方案3】:

    如果您的属性是真正独立的,那么为每个属性使用独立的锁并没有什么坏处。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-29
      • 1970-01-01
      • 2020-10-25
      • 1970-01-01
      相关资源
      最近更新 更多