我一直在寻找的.NET TPL的“数据流”库的某些部分出于好奇的执行情况和我遇到下面的代码片段来了:线程安全
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
)是挥发性。在我的理解这给出了两个保证:
- 每次所有四个字段被读取,就必须读取顺序
- 编译器可能没有的Elid任何的读取和CLR/JIT必须采取措施为了防止这些值
的“高速缓存”从我读给定方法中,发生以下情况:
- 的
ConcurrentQueue
的内部状态的初始读取被执行(THA t是head
,tail
,head.Low
和tail.High
)。 - 执行单个忙等待旋
- 然后,该方法再次,并检查读出的内部状态的任何变化
- 如果状态已改变,则转到步骤2,重复
- 返回读取状态一旦它被认为是'稳定的'
现在假设全部正确,我的“问题”是这样的:上述状态的读取不是原子的。我没有看到阻止半写入状态的读取(例如,写入器线程已更新head
但尚未tail
)。
现在我有点意识到,像这样的缓冲区中的半写状态不是世界的尽头 - 在所有的head
和tail
指针完全可以独立更新/读取之后,通常在CAS /自旋循环。
但是,我真的不知道什么时候旋转一下然后再读一遍。你真的要在一次旋转的时间里“捕捉”正在进行的改变吗?它试图“防范”什么?换句话说:如果整个状态读取的目的是为原子的,我不认为该方法做了什么帮助,如果没有,那么究竟是什么是该方法在做什么?
那么最终呢,这个方法的“双重检查”基本上没有意义么?或者我误解了? – Xenoprimate
可能需要的唯一检查就是'_index',它可以避免在'Segment._next'链中返回不指向'_tail'的'_head'。我说*可能*,因为在'_head'的易失性读取之后,不应该可以观察到具有'_tail' **的易失性读取,因为在不改变Segment._next '链,并在最后加上严格增加的指数。其他检查有一定程度的稳定性(请参阅while语句前的注释)。 – acelent