我正在阅读 Joe Duffy在 Windows 上的并发编程。在“Memory Models and Lock Freedom”一章的最后,他给出了一个lock free stack的例子。我已经阅读了代码,但有一点我不明白,那就是需要将m_next
字段标记为volatile
. 因为有一个完整的内存屏障Interlocked.CompareExchange
对吗?有没有人有想法?
我已经粘贴了下面的示例代码。
class LockFreeStack<T>
{
class Node
{
internal T m_value;
internal volatile Node m_next;
}
volatile Node m_head;
void Push(T value)
{
Node n = new Node();
n.m_value = value;
Node h;
do
{
h = m_head;
n.m_next = h;
}
while (Interlocked.CompareExchange(ref m_head, n, h) != h);
}
T Pop()
{
Node n;
do
{
n = m_head;
if (n == null) throw new Exception('stack empty');
}
while (Interlocked.CompareExchange(ref m_head, n.m_next, n) != n);
return n.m_value;
}
}
我会给我的两分钱,但不能 100% 确定我要说的是正确的。Joe Duffy是世界级的多线程专家,但我认为在这个实现中,他对LockFreeStack<T>
类内部状态的跨线程可见性过于谨慎。Node.m_next
在我看来,该领域的波动性是多余的。该Node
实例是可变的,但他们只突变之前,他们在内部链接列表链接。在那个阶段之后,它们实际上是不可变的。该可变阶段由单个线程执行,因此另一个线程不可能看到Node
实例的陈旧版本。
这使得只有指令的可能性重新排序,为理由,宣布Node.m_next
为volatile
。由于n.m_next = h;
分配夹在读取另一个易失性字段 ( m_head
) 和Interlocked.CompareExchange
操作之间,我假设已经阻止了可能危及实现正确性的指令重新排序,但正如我所说,我不是 100%当然。
我很确定LockFreeStack<T>
该类的实现可以通过使Node
类不可变来提高性能,但代价是稍微分配更多。如今(C# 9)这可以通过从 type 切换到 typeclass
来实现record
。这是这样一个实现的样子:
class LockFreeStack<T>
{
record Node(T Value, Node Next) { }
Node _head;
void Push(T value)
{
Node h = Volatile.Read(ref _head);
while (true)
{
Node n = new Node(value, h);
var original = Interlocked.CompareExchange(ref _head, n, h);
if (original == h) break;
h = original;
}
}
T Pop()
{
Node h = Volatile.Read(ref _head);
while (true)
{
if (h == null) throw new Exception("Stack empty");
var original = Interlocked.CompareExchange(ref _head, h.Next, h);
if (original == h) break;
h = original;
}
return h.Value;
}
}
请注意,每个操作Push
或Pop
操作只产生一次波动性成本。甚至可以争辩说Volatile.Read
,该_head
字段的 可以完全省略,因为_head
无论如何都会通过第一次Interlocked.CompareExchange
操作纠正可能的陈旧值,只需要额外的循环迭代while (true)
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句