我出于好奇查看了.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;
}
}
(可在此处查看:https://github.com/dotnet/corefx/blob/master/src/System.Threading.Tasks.Dataflow/src/Internal/ConcurrentQueue.cs#L345)
据我所了解的线程安全,这个操作容易出现数据竞争。我将解释我的理解,然后是我认为的“错误”。当然,我觉得更可能是我的思维模型有误而不是库本身存在问题,希望这里的某个人能指出我哪里错了。
...
所有给定的字段(
head
、tail
、head.Low
和tail.High
)都是易失性的。在我看来,这提供了两个保证:
- 每次读取所有四个字段时,它们必须按顺序读取
- 编译器不能省略任何一次读取,CLR / JIT必须采取措施防止值被“缓存”
- 执行
ConcurrentQueue
的内部状态的初始读取(即head
、tail
、head.Low
和tail.High
)。 - 进行单个忙等待旋转
- 然后再次读取内部状态并检查是否有任何更改
- 如果状态已更改,请返回第2步并重复
- 在被认为是“稳定”的情况下返回读取状态
head
但尚未更新tail
)的限制。现在我有些意识到,在这样的缓冲区中,半写状态并不是世界末日——毕竟,
head
和tail
指针在CAS /自旋循环中通常可以独立更新/读取。但是,我真的想知道旋转一次然后再次读取的目的是什么?你真的能在进行单次旋转的时间内“捕获”正在发生的更改吗?它试图“防护”什么?换句话说:如果整个状态读取需要是原子的,我认为该方法没有任何帮助,如果不是,那么该方法到底是在做什么?
_index
,它避免了在Segment._next
链中返回一个不指向_tail
的_head
。我说“可能”,因为在读取_head
后读取_tail
的 volatile 读取时,不应该能够观察到这一点,因为段从开头删除而不更改Segment._next
链,并且以严格递增的索引添加到末尾。其他检查是为了某种稳定性措施(请参见while
语句之前的注释)。 - acelent