在实现线程安全队列或列表时,是否需要在返回Count之前锁定?

时间:2010-10-28 21:15:45

标签: c# multithreading locking thread-safety

实现线程安全列表或队列时;它是否需要在返回Count之前锁定List.Count属性,即:

//...
public int Count 
{
    lock (_syncObject)
    {
       return _list.Count;
    }
}
//...

是否有必要进行锁定,因为原始_list.Count变量可能不是易变变量?

3 个答案:

答案 0 :(得分:5)

是的,这是必要的,但大多不相关。如果没有锁定,您可能会读取陈旧的值,甚至根据内部工作和_list.Count的类型,引入错误。

但请注意,使用Count属性是有问题的,任何调用代码都不能真正依赖它:

if (myStore.Count > 0)  // this Count's getter  locks internally 
{
    var item = myStore.Dequeue(); // not safe, myStore could be empty
}

因此,您应该设计一个设计,其中检查计数并对其进行组合:

ItemType GetNullOrFirst()
{
    lock (_syncObject)
    {
       if (_list.Count > 0)
       {
           ....
       }
    }
}

其他:

  

因为原始的_list.Count变量可能不是一个易变的变量,所以有必要进行锁定吗?

_list.Count不是变量,而是属性。它不能标记为易变。它是否是线程安全的取决于属性的getter代码,但它通常是安全的。但不可靠。

答案 1 :(得分:2)

取决于。

现在,从理论上讲,因为底层值不是线程安全的,所以任何事情都可能因为实现的内容而出错。但在实践中,它是从32位变量读取,因此保证是原子的。因此它可能是陈旧的,但它将是一个真正的陈旧价值而不是由于在变化之前读取半个值而在之后读取另一半而导致的垃圾。

所以问题是,陈腐是否重要?可能它没有。你越能忍受陈旧性越好表现(因为你能忍受的越多,你需要做的就越少,以确保你没有陈旧的东西)。例如。如果你把数量计算到用户界面并且收集正在快速变化,那么只需要人们阅读数字并在他们自己的大脑中处理数据所需的时间就足以使其无论如何都要过时,因此陈旧。

但是,如果你需要它来确保在尝试之前操​​作是合理的,那么这种陈旧性将导致问题。

然而,无论如何,这种陈旧性将会发生,因为在你的锁定(并保证是新鲜的)读取和正在发生的操作之间,收集有机会改变,所以你甚至在遇到竞争条件时锁定。

因此,如果新鲜度很重要,那么您将不得不锁定更高的级别。在这种情况下,较低级别的锁定只是浪费。因此,你可能在那时没有它。

重要的是,即使有一个在各方面都是线程安全的类,最好的可以保证每个操作都是新鲜的(尽管可能无法保证;实际上当涉及多个核心时) “新鲜”开始变得毫无意义,因为接近真正同时发生的变化可能发生)并且每个操作都不会将对象置于无效的安全中。仍然可以使用线程安全对象编写非线程安全代码(事实上,很多非线程安全对象由int,字符串和bool或者由它们组成的对象组成,并且每个对象都是线程安全的)

用于多线程使用的可变类有用的一件事是同步“Try”方法。例如。要将List用作堆栈,我们需要执行以下操作:

  1. 查看列表是否为空,如果是,则报告失败。
  2. 获得“最高”值。
  3. 删除最高值。
  4. 即使我们单独同步每一个,这样做的代码也不会是线程安全的,因为每个步骤之间可能发生一些事情。但是,我们可以提供Pop()方法,该方法在一个同步方法中执行每个三个方法。类似的方法对于无锁类也很有用(其中使用不同的技术来确保方法成功或完全失败,并且不会损坏对象)。

答案 2 :(得分:2)

不,你不需要锁定,但调用者应该会发生类似这样的事情

count is n
thread asks for count
Count returns n
another thread dequeues
count is n - 1
thread asking for count sees count is n when count is actually n - 1