在C#/.NET Little Wonders article for ConcurrentStack / Concurrent Queue上,提到了.Count
上ConcurrentStack
操作的以下内容:
Count获取堆栈的快照 然后计算项目。这意味着 如果你只是,它是一个O(n)操作 想要检查空堆栈,请致电 IsEmpty而不是O(1)。
我在多线程编程方面还不是很有经验,但我明白为什么你不能只是遍历集合中的项目并计算它们,因为集合可能同时被其他线程更改。但是,如果您必须锁定ConcurrentStack
足够长的时间来制作快照,那么在锁定项目时计算项目是否更容易,返回该计数并释放锁定,而不是在释放锁之前生成整个快照的开销和时间成本?
我可能会遗漏一些关于它如何运作的基本信息,我将不胜感激您的任何想法或见解。
答案 0 :(得分:5)
我不知道它是否像这样实现,但如果你将它实现为具有不可变节点的单链表,则快照几乎是免费的,不需要锁定。你只需要获得当前的顶级元素。然后按照链接列表开头。
您可以将每个节点的位置保存在节点的堆栈上,但是为了Count
的性能而交换内存。并且可能通常不会调用count来保证每个节点有额外的内存。
大多数ConcurrentCollection完全没有锁定。例如,可以使用Interlocked.CompareExchange
在指向当前节点的字段上构建这种链表支持的堆栈。如果你使一些操作无锁,你通常需要使所有这些操作无锁,因为无锁操作不会尊重锁。
答案 1 :(得分:3)
我不确定,但这是一个猜测。
堆栈可以实现为单链表,由不可变单元组成,其中每个单元指向堆栈下面的单元。那么“快照”就是制作堆栈顶部的副本;因为单元格是不可变的,所以它也会拉动当前堆栈的其余部分。
因此,快照将是O(1)操作,但计算实际项目仍为O(n)。
答案 2 :(得分:3)
你假设为了指望一个快照,它必须拿出一个大锁。
我相信并发集合的设计考虑了廉价 - 可能是乐观的 - 快照。例如,如果您通过ConcurrentStack
遍历GetEnumerator()
,那么它也会使用快照。我非常怀疑创建该快照始终是O(n)操作。
答案 3 :(得分:1)
ConcurrentStack和ConcurrentQueue是线程安全集合,因此不涉及锁定。例如,在ConcurrentStack中,通过读取头节点然后遍历列表累积计数来计算计数
答案 4 :(得分:0)
我认为最大的原因是要求一个并发队列(其中两个或多个线程总是在操纵内容)来计算项目数是一个有缺陷的概念。我怀疑他们只提供了Count属性以符合现有的接口。因此,如果api存在的唯一原因是满足接口要求,那么谁在乎它的表现,因为你不应该开始使用它。
当处理由多个线程修改的对象时,向对象询问另一个线程可以更改的某个状态/值是不行的。因为当您的问题得到解答时,它可能不再是正确的答案。这类似于WinForm的“InvokeRequired”属性中的基本问题。有些API在多线程环境中只是一个坏主意,我认为这个Count属于那个类别。
最终,实现者没有为ICollection.Count属性使用显式接口成员而不是将其公开,这有点奇怪。那么至少你知道你不应该使用它。他们确实指定在MSDN上使用IsEmpty;然而,很容易忽视这一点。