这个锁用法线程安全吗?

时间:2012-01-19 10:29:47

标签: c# synchronization locking

我知道使用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中的锁定块中的列表仍然会发生变化吗?

9 个答案:

答案 0 :(得分:10)

不,这不是线程安全的。添加和计数可以在“相同”时间执行。您有两个不同的锁定对象。

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

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

答案 1 :(得分:3)

不,这不是线程安全的。 A.MethodeAOtherClass.CallToMethodInOtherClass锁定在不同的对象上,因此它们不是互斥的。如果您需要保护对列表的访问权限,请不要将其传递给外部代码,将其保密。

答案 2 :(得分:3)

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

希望这有帮助

答案 3 :(得分:2)

不,这不是线程安全的。

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

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

如果该方法会在列表中进行更改,则会出现令人讨厌的问题。要解决它,请更改MethodeB:

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

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

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

一种简单的方法是使用静态锁定对象f.ex:

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

并在两个类中都使用锁定:

lock(A.lockObj)
{
}

答案 6 :(得分:1)

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

  1. 线程安全的代码片段不是线程安全的应用程序。您可以将不同的线程安全操作组合到非线程安全的操作中。实际上,许多非线程安全的代码可以分解成更小的部分,所有部分都是自己的线程安全的。

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

  3. 此代码是线程安全的:

    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,因此代码可以在Remove之间调用ClearCount,以确保list[93]工作,list[93]被召唤。

    现在,如果我们知道list只被添加到了,那很好,即使同时发生调整大小,我们也会得到list[93]的值无论哪种方式。如果某些内容正在写入list[93]并且它是一种.NET会以原子方式写入的类型(并且int就是这样一种类型),那么我们最终会得到旧版本或者新的,就像我们正确锁定一样,我们可以获得旧的或新的,具体取决于哪个线程先锁定。 同样,这是一个实现细节而不是指定的承诺,我只是指出这样做只是为了指出线程安全性仍然会导致非线程安全的代码。

    将其转向真实代码。我们不应该假设list.Countlist[93]是线程安全的,因为我们没有承诺他们将会改变,但即使我们确实有这个承诺,这两个承诺也会赢得& #39; t加起来承诺他们一起是线程安全的。

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

    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 isn&#39并不重要; t线程安全,因为唯一可以访问myList的代码带来了自己的保证,不会与myList上的其他操作同时调用它。

    两件重要的事情是:

    1. 当某些内容被描述为&#34;线程安全&#34;时,要知道它的前景是什么,因为有不同类型的承诺属于&#34;线程 - 安全&#34;而且它本身就意味着&#34;我不会把这个物体变成荒谬的状态&#34;虽然它是一个重要的构建块,但它本身并不是很多。

    2. 锁定操作的,对每个影响相同数据的组使用相同的锁定,并保护对对象的访问权限,以便无法访问可能是另一个不打球的线程。

    3. *这是一个非常有限的线程安全定义。在list[93]上调用List<T>,其中T是一种将以原子方式书写和阅读的类型,我们不知道它是否实际上至少有94项同样安全无论是没有其他线程在运行。当然,在任何一种情况下它都可以抛出ArgumentOutOfRangeException的事实并不是大多数人会考虑的事情,而是安全&#34;,但我们对多线程的保证与一个保持相同。我们通过在单个线程中检查Count但不是在多线程情况下获得更强的保证,这导致我将其描述为不是线程安全的;虽然那个组合仍然不会成为腐败状态,但它可以导致例外情况,我们确信自己无法发生。

答案 7 :(得分:0)

可能是最简单的方法

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;
   }
  }
}

答案 8 :(得分:0)

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

但是,你真的应该尽量避免这种静态锁定。在多个线程使用静态锁的情况下创建死锁会很容易。

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

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