我正在研究.NET TPL" Dataflow"的某些部分的实现。库出于好奇,我遇到了以下片段:
private void GetHeadTailPositions(out Segment head, out Segment tail,
out int headLow, out int tailHigh)
{
head = _head;
tail = _tail;
headLow = head.Low;
tailHigh = tail.High;
SpinWait spin = new SpinWait();
//we loop until the observed values are stable and sensible.
//This ensures that any update order by other methods can be tolerated.
while (
//if head and tail changed, retry
head != _head || tail != _tail
//if low and high pointers, retry
|| headLow != head.Low || tailHigh != tail.High
//if head jumps ahead of tail because of concurrent grow and dequeue, retry
|| head._index > tail._index)
{
spin.SpinOnce();
head = _head;
tail = _tail;
headLow = head.Low;
tailHigh = tail.High;
}
}
根据我对线程安全的理解,此操作容易发生数据竞争。我将解释我的理解,然后我认为是“错误”。当然,我认为我的心理模型中的错误更可能是图书馆中的错误,我希望有人能指出我出错的地方。
...
所有给定字段(head
,tail
,head.Low
和tail.High
)都是易变的。根据我的理解,这给出了两个保证:
根据我读到的给定方法,会发生以下情况:
ConcurrentQueue
的内部状态的初始读取(即head
,tail
,head.Low
和tail.High
)。现在假设这一切都是正确的,我的问题"因此:上述状态的读取不是原子的。我没有看到任何阻止读取半写状态的内容(例如,作者线程已更新head
但尚未tail
。)
现在我有点意识到,在这样的缓冲区中半写状态不是世界末日 - 在所有head
和tail
指针完全正常之后独立更新/读取,通常在CAS /旋转循环中。
然而,我并没有真正看到旋转一次然后重读的重点。你是否真的要抓住'在进行单次旋转所需的时间内进行了改变?它试图保护什么'反对?换句话说:如果整个状态读取都是原子的,我不认为该方法可以做任何事情来帮助它,如果不是, 该方法究竟做了什么?
答案 0 :(得分:2)
您是对的,但请注意,GetHeadTailPositions
中的输出值稍后会用作ToList
,Count
和GetEnumerator
中的快照。
更令人担忧的是并发队列might hold on to values indefinitely。当私有字段ConcurrentQueue<T>._numSnapshotTakers
不为零时,它会阻止对条目进行归零或将其设置为值类型的默认值。
Stephen Toub在ConcurrentQueue<T> holding on to a few dequeued elements中发表了关于此事的博客:
无论好坏,.NET 4中的这种行为实际上是“按设计”。其原因与枚举语义有关。 ConcurrentQueue&LT; T&GT;为枚举提供“快照语义”,意味着您开始枚举的瞬间,ConcurrentQueue&lt; T&gt;捕获队列中当前所有内容的当前头部和尾部,即使这些元素在捕获后出列,或者如果新元素在捕获后排队,枚举仍将返回全部且仅返回当时队列中的内容枚举开始了。如果细分中的元素在出列时被删除,那将影响这些枚举的准确性。
对于.NET 4.5,我们改变了设计,以达到我们认为的良好平衡。除非出现并发枚举,否则现在将出列的元素排除,除非发生并发枚举,在这种情况下,元素不会被清除,并且会显示与.NET 4中相同的行为。因此,如果您从不枚举ConcurrentQueue&lt; T&gt;,则队列将导致队列立即丢弃其对出队元素的引用。只有当发出出队时,有人恰好在列举队列(即在队列中调用了GetEnumerator并且没有遍历整个枚举器或者已经处理掉它),否则不会发生无效;与.NET 4一样,此时引用将保留,直到删除包含的段。
从源代码中可以看到,获取一个枚举器(通过通用GetEnumerator<T>
或非通用GetEnumerator
),调用ToList
(或ToArray
使用ToList
)或TryPeek
可能会导致在删除项目后仍保留引用。不可否认,TryDequeue
(称为ConcurrentQueue<T>.Segment.TryRemove
)和TryPeek
之间的竞争条件可能很难激起,但它就在那里。