gpt4 book ai didi

c# - .NET TPL 数据流源中的线程安全

转载 作者:行者123 更新时间:2023-11-30 12:22:16 30 4
gpt4 key购买 nike

出于好奇,我正在查看 .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;
}
}

(可在此处查看:https://github.com/dotnet/corefx/blob/master/src/System.Threading.Tasks.Dataflow/src/Internal/ConcurrentQueue.cs#L345)

根据我对线程安全的理解,此操作容易出现数据竞争。我将解释我的理解,然后解释我认为的“错误”。当然,我认为这更有可能是我心智模型的错误,而不是图书馆的错误,我希望这里有人能指出我哪里出错了。

...

所有给定的字段(headtailhead.Lowtail.High)都是可变的.在我看来,这提供了两个保证:

  • 每次读取所有四个字段时,必须按顺序读取
  • 编译器不能忽略任何读取,CLR/JIT 必须采取措施防止值的“缓存”

根据我对给定方法的了解,会发生以下情况:

  1. 执行ConcurrentQueue 内部状态的初始读取(即headtailhead.Lowtail.High)。
  2. 执行单个忙等待自旋
  3. 然后该方法再次读取内部状态并检查是否有任何更改
  4. 如果状态发生变化,转到第2步,重复
  5. 一旦被认为“稳定”就返回读取状态

现在假设这一切都是正确的,因此我的“问题”是:上面的状态读取不是原子的。我没有看到任何阻止读取半写状态的东西(例如,编写器线程已更新 head 但尚未更新 tail)。

现在我有点意识到像这样的缓冲区中的半写状态并不是世界末日 - 毕竟 headtail 指针是完全可以独立更新/读取,通常在 CAS/自旋循环中。

但后来我真的不明白旋转一次然后再阅读的意义何在。您真的要在单次旋转所需的时间内“捕捉”正在进行的变化吗?它试图“防范”什么?换句话说:如果整个状态读取是原子的,我不认为该方法有任何帮助,如果没有,该方法到底在做什么?

最佳答案

你是对的,但请注意 GetHeadTailPositions 的输出值稍后用作 ToList 中的快照, CountGetEnumerator .

更令人担忧的是并发队列might hold on to values indefinitely .当私场ConcurrentQueue<T>._numSnapshotTakers不为零,它可以防止清空条目或将它们设置为值类型的默认值。

Stephen Toub 在 ConcurrentQueue<T> holding on to a few dequeued elements 中对此发表了博客:

For better or worse, this behavior in .NET 4 is actually “by design.” The reason for this has to do with enumeration semantics. ConcurrentQueue<T> provides “snapshot semantics” for enumeration, meaning that the instant you start enumerating, ConcurrentQueue<T> captures the current head and tail of what’s currently in the queue, and even if those elements are dequeued after the capture or if new elements are enqueued after the capture, the enumeration will still return all of and only what was in the queue at the time the enumeration began. If elements in the segments were to be nulled out when they were dequeued, that would impact the veracity of these enumerations.

For .NET 4.5, we’ve changed the design to strike what we believe to be a good balance. Dequeued elements are now nulled out as they’re dequeued, unless there’s a concurrent enumeration happening, in which case the element isn’t nulled out and the same behavior as in .NET 4 is exhibited. So, if you never enumerate your ConcurrentQueue<T>, dequeues will result in the queue immediately dropping its reference to the dequeued element. Only if when the dequeue is issued someone happens to be enumerating the queue (i.e. having called GetEnumerator on the queue and not having traversed the enumerator or disposed of it yet) will the null’ing out not happen; as with .NET 4, at that point the reference will remain until the containing segment is removed.

从源代码中可以看出,获取枚举器(通过通用 GetEnumerator<T> 或非通用 GetEnumerator ),调用 ToList (或 ToArray 使用 ToList )或 TryPeek可能导致即使在删除项目后仍保留引用。诚然,TryDequeue 之间的竞争条件(调用 ConcurrentQueue<T>.Segment.TryRemove )和 TryPeek可能很难激怒,但它就在那里。

关于c# - .NET TPL 数据流源中的线程安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41552532/

30 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com