【问题标题】:Is this lock usage thread safe?这个锁使用线程安全吗?
【发布时间】:2012-02-14 00:29:18
【问题描述】:

我知道使用lock(this) 或任何共享对象是错误的。

不知这样的用法可以吗?

public class A
{
  private readonly object locker = new object();
  private List<int> myList;
  public A()
  {
    myList = new List<int>()
  }

  private void MethodeA()
  {
    lock(locker)
    {
      myList.Add(10);
    }
  }

  public void MethodeB()
  {
    CallToMethodInOtherClass(myList);
  }
}

public class OtherClass
{
  private readonly object locker = new object();
  public CallToMethodInOtherClass(List<int> list)
  {
   lock(locker)
   {
     int i = list.Count;
   }
  }
}

这个线程安全吗?在OtherClass 中,我们使用私有对象进行锁定,因此如果class A 锁定及其私有锁定,列表是否仍然可以在OtherClass 中的锁定块中更改?

【问题讨论】:

  • 您的浴室有两扇门,每扇门都有一把锁。您的问题是“假设我只在洗澡时锁第一把锁,而我的朋友鲍勃只在洗澡时锁第二把锁。我们两个人可以同时洗澡吗?”显然是的!如果您和 Bob 想避免一起洗澡,那么您需要同意双方使用同一个锁。你不能像这样访问线程安全的对象。

标签: c# synchronization locking


【解决方案1】:

所有的答案都说这些是不同的锁对象。

一个简单的方法是拥有一个静态锁对象 f.ex:

publc class A
{
    public static readonly object lockObj = new object();
}

并且在这两个类中都使用锁:

lock(A.lockObj)
{
}

【讨论】:

  • 您应该避免将锁定对象暴露为 public。如果您的意图仅仅是线程同步,则锁定对象应该是私有的。
【解决方案2】:

它实际上是线程安全的(纯粹作为Count 上的实现细节问题),但是:

  1. 代码的线程安全 sn-ps 不是线程安全应用程序。您可以将不同的线程安全操作组合成非线程安全操作。事实上,很多非线程安全的代码都可以分解成更小的部分,所有这些部分本身都是线程安全的。

  2. 由于您希望的原因,它不是线程安全的,这意味着进一步扩展它不会是线程安全的。

这段代码是线程安全的:

public void CallToMethodInOtherClass(List<int> list)
{
   //note we've no locks!
   int i = list.Count;
   //do something with i but don't touch list again.
}

使用任何列表调用它,它会根据该列表的状态为i 提供一个值,而不管其他线程在做什么。它不会损坏list。它不会给i 一个无效值。

所以虽然这段代码也是线程安全的:

public void CallToMethodInOtherClass(List<int> list)
{
  Console.WriteLine(list[93]); // obviously only works if there's at least 94 items
                            // but that's nothing to do with thread-safety
}

这段代码不是线程安全的:

public void CallToMethodInOtherClass(List<int> list)
{
   lock(locker)//same as in the question, different locker to that used elsewhere.
   {
     int i = list.Count;
     if(i > 93)
       Console.WriteLine(list[93]);
   }
}

在进一步讨论之前,我描述为线程安全的两个位并未被 List 规范所承诺。保守的编码会假设它们不是线程安全的,而不是依赖于实现细节,但我将依赖于实现细节,因为它以一种重要的方式影响了如何使用锁的问题:

因为在list 上运行的代码没有首先获取locker 上的锁,所以不会阻止该代码与CallToMethodInOtherClass 并发运行。现在,虽然list.Count 是线程安全的,list[93] 是线程安全的,* 两者的组合我们依赖第一个来确保第二个工作不是线程安全的。因为锁外的代码会影响list,所以代码可以在Count 之间调用Remove 或Clear,以确保list[93] 可以工作,而list[93] 会被调用。

现在,如果我们知道 list 只会被添加到,那很好,即使同时发生调整大小,我们最终都会得到 list[93] 的值。如果有东西正在写入list[93],并且它是 .NET 将自动写入的一种类型(int 就是这样一种类型),我们最终会得到旧的或新的,就像我们' d 正确锁定我们会得到旧的或新的,这取决于哪个线程先锁定。 再次重申,这是一个实现细节,而不是指定的承诺,我这样说只是为了指出给定的线程安全性如何仍然导致非线程安全代码。

将其移向真实代码。我们不应该假设 list.Count 和 list[93] 是线程安全的,因为我们没有被保证它们会是线程安全的,而且这可能会改变,但即使我们确实有这个承诺,这两个承诺加起来也不会成为一个承诺它们在一起是线程安全的。

重要的是使用相同的锁来保护可能相互干扰的代码块。因此,请考虑以下保证线程安全的变体:

public class ThreadSafeList
{
  private readonly object locker = new object();
  private List<int> myList = new List<int>();

  public void Add(int item)
  {
    lock(locker)
      myList.Add(item);
  }
  public void Clear()
  {
    lock(locker)
      myList.Clear();
  }
  public int Count
  {
    lock(locker)
      return myList.Count;
  }
  public int Item(int index)
  {
    lock(locker)
      return myList[index];
  }
}

保证这个类在它所做的所有事情上都是线程安全的。在不依赖任何实现细节的情况下,这里没有任何方法会破坏状态或给出不正确的结果,因为另一个线程正在处理同一个实例。以下代码仍然不起作用:

// (l is a ThreadSafeList visible to multiple threads.
if(l.Count > 0)
  Console.WriteLine(l[0]);

我们已经保证每个调用的线程安全 100%,但是我们没有保证组合,我们不能保证组合。

我们可以做两件事。我们可以为组合添加一个方法。对于专门为多线程使用而设计的许多类,以下内容很常见:

public bool TryGetItem(int index, out int value)
{
  lock(locker)
  {
    if(l.Count > index)
    {
      value = l[index];
      return true;
    }
    value = 0;
    return false;
  }
}

这使得计数测试和项目检索成为保证线程安全的单个操作的一部分。

或者,通常我们需要做的是,我们在操作分组的地方发生锁定:

lock(lockerOnL)//used by every other piece of code operating on l
  if(l.Count > 0)
    Console.WriteLine(l[0]);

当然,这会使ThreadSafeList 内的锁变得多余,而且只是浪费精力、空间和时间。这是大多数类不为其实例成员提供线程安全的主要原因 - 因为您无法有意义地保护类内部对成员的调用组,除非线程安全承诺,否则尝试这样做是浪费时间非常详细,并且非常有用。

回到问题中的代码:

CallToMethodInOtherClass 中的锁定应该被移除,除非OtherClass 有自己的内部锁定原因。它不能做出有意义的承诺,即它不会以非线程安全的方式组合,并且向程序添加更多锁只会增加分析它以确保没有死锁的复杂性。

对CallToMethodInOtherClass 的调用应该受到与该类中其他操作相同的锁的保护:

public void MethodeB()
{
  lock(locker)
    CallToMethodInOtherClass(myList);
}

那么只要CallToMethodInOtherClass 没有将myList 存储在稍后其他线程可以看到的地方,CallToMethodInOtherClass 不是线程安全的也没关系,因为唯一可以访问的代码myList 提供了自己的保证,不会与myList 上的其他操作同时调用它。

两个重要的事情是:

  1. 当某事被描述为“线程安全”时,请知道它的承诺是什么,因为有不同类型的承诺属于“线程安全”,它本身就意味着“我赢了'不要让这个对象进入一个荒谬的状态”,虽然它是一个重要的构建块,但它本身并不是很多。

  2. 锁定操作的组,每个组使用相同的锁,这将影响相同的数据,并保护对对象的访问,以便不可能有另一个线程不玩这个。

*这是一个非常有限的线程安全定义。在List&lt;T&gt; 上调用list[93],其中T 是一种将被原子读写的类型,我们不知道它实际上是否至少有94 个项目同样安全,无论是否有其他线程在其上运行.当然,在任何一种情况下它都可以抛出ArgumentOutOfRangeException这一事实并不是大多数人认为的“安全”,但我们对多线程的保证与一个线程相同。我们通过在单线程而不是在多线程情况下检查Count 获得了更强的保证,这导致我将其描述为不是线程安全的;虽然该组合仍然不会破坏状态,但它可能会导致我们向自己保证不会发生的异常。

【讨论】:

  • 改用 ConcurrentBag 或 ConcurrentDictionary,这样就不用担心锁定了。
  • @PålThingbø 一点也不真实。首先,虽然这些集合,实际上是我自己的thread-safe dictionary and set 和其他一些这样的集合,对它们的每个单独操作都是线程安全的,但由于我上面给出的原因,对它们的操作组不一定是线程安全的(想想看, int 是线程安全的,但这并不能使++x 成为线程安全的,因为这三个线程安全的操作作为一个单元不是线程安全的)。建议某人在需要列表时使用包或字典也很少有意义。 ……
  • @PålThingbø 这些类的语义完全不同。我希望很快会发布一个线程安全的列表类,它可以提供比上面答案中更好的并发行为,尽管它仍然会受到限制(没有RemoveAt 或Insert;我可能会生成一个包含它们的不同类太晚了,但是在没有锁定的情况下使它们成为线程安全的比其他的要麻烦得多),并且它仍然不会使使用它的代码神奇地作为一个整体线程安全,就像ConcurrentDictionary 一样,再次出于原因我在上面给出。
  • 感谢@Jon 的澄清。我是根据有问题的代码提供建议的,这只是在集合中添加和删除项目。 Concurrent 是一个很好的工具。
  • @PålThingbø 因为问题中的代码只是添加了值10,所以最好的完全替换是只计算添加了多少10s!就实际代码而言,我想您对它们不需要列表语义的猜测与我对它们的猜测一样好。尽管如此,并发对象仍然需要注意确保对它们执行的一组操作作为一个整体仍然是线程安全的。
【解决方案3】:

不,这不是线程安全的。为了使其线程安全,您可以在 static 对象上使用锁定,因为它们在线程之间共享,这可能会导致代码中的死锁,但可以通过保持正确的锁定顺序来处理。 lock 会带来性能成本,因此请明智地使用它。

希望对你有帮助

【讨论】:

  • 这不是线程安全的。除了锁定对象超出必要范围之外,您还公开了锁定对象,因此现在不同的外部代码可以与其他锁定对象一起以不同的方式使用它,从而使死锁成为可能。
  • +1 没错,我是在解释static对象在lock的情况下的使用,理想情况下所有锁对象都应该有有限的范围。
  • @AmarPalsapure:非竞争锁的性能成本为 20 到 80 纳秒。非竞争锁通常不是性能问题。
  • @AmarPalsapure:哈希表通常不是线程安全的。有些对于readers only场景可能是线程安全的;有关详细信息,请参阅文档。
  • @Maya:共享对象不会导致死锁。 不正确的锁顺序会导致死锁。
【解决方案4】:

许多答案都提到了使用静态只读锁。

但是,您确实应该尽量避免这种静态锁定。在多个线程使用静态锁的情况下,很容易造成死锁。

您可以使用 .net 4 并发集合之一,它们确实代表您提供了一些线程同步,因此您不需要使用锁定。

查看System.collections.Concurrent 命名空间。 对于此示例,您可以使用 ConcurrentBag&lt;T&gt; 类。

【讨论】:

    【解决方案5】:

    可能是最简单的方法

    public class A
    {
      private List<int> myList;
      public A()
      {
        myList = new List<int>()
      }
    
      private void MethodeA()
      {
        lock(myList)
        {
          myList.Add(10);
        }
      }
    
      public void MethodeB()
      {
        CallToMethodInOtherClass(myList);
      }
    }
    
    public class OtherClass
    {
      public CallToMethodInOtherClass(List<int> list)
      {
       lock(list)
       {
         int i = list.Count;
       }
      }
    }
    

    【讨论】:

    • 但我认为用 shraed 对象锁定是错误的
    【解决方案6】:

    不,这不是线程安全的。 A.MethodeA 和 OtherClass.CallToMethodInOtherClass 锁定不同的对象,因此它们不是互斥的。如果您需要保护对列表的访问,请不要将其传递给外部代码,保持私有。

    【讨论】:

      【解决方案7】:

      不,它不是线程安全的。 Add 和 Count 可以“同时”执行。您有两个不同的锁定对象。

      传递列表时始终锁定自己的锁对象:

        public void MethodeB()
        {
          lock(locker)
          {
            CallToMethodInOtherClass(myList);
          }
        }
      

      【讨论】:

      • 它实际上是线程安全的,尽管只是作为Count 的实现细节,它没有被承诺是线程安全的,并且不能很好地扩展到更现实的代码中。
      【解决方案8】:

      不,他们必须锁定同一个对象。使用您的代码,它们都锁定在不同的位置,并且每个调用都可以同时执行。

      为了使代码线程安全,在 MethodeB 中放置一个锁或使用列表本身作为锁对象。

      【讨论】:

      • @Felix 我认为用共享对象锁定是错误的,所以用列表本身锁定它是错误的吗?
      • @Maya 是的,这是错误的。但这是解决所描述问题的简单方法。但无论如何,正确的方法是创建一个线程安全列表或包含该列表的容器,并仅通过容器访问该列表。
      【解决方案9】:

      不,这不是线程安全的。

      您的 2 种方法正在锁定 2 个不同的对象,它们不会相互锁定。

      因为CallToMethodInOtherClass() 只检索 Count 的值,所以不会出错。但是它周围的lock() 是无用的和误导性的。

      如果该方法会在列表中进行更改,您将遇到一个令人讨厌的问题。要解决它,改变MethodeB:

        public void MethodeB()
        {
          lock(locker)  // same instance as MethodA is using
          {
            CallToMethodInOtherClass(myList);
          }
        }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-03-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多