关于Interlocked
与volatile
的问题有很多相关的问题,我理解并了解volatile
的概念(没有重新排序,总是从记忆中读取,等等,我知道Interlocked
如何工作,因为它执行原子操作。
但我的问题是:假设我有一个从多个线程中读取的字段,这是一些引用类型,例如:public Object MyObject;
。我知道如果我对它进行比较交换,就像这样:Interlocked.CompareExchange(ref MyObject, newValue, oldValue)
互锁保证仅将newValue
写入ref MyObject
引用的内存位置,如果ref MyObject
而oldValue
目前指的是同一个对象。
但阅读怎么样? Interlocked
保证在MyObject
操作成功后阅读CompareExchange
的任何帖子都会立即获得新值,或者我必须将MyObject
标记为volatile
确保这个?
我想知道的原因是我已经实现了一个无锁链接列表,当你为它添加一个元素时,它会不断更新其内部的“head”节点,如下所示:
[System.Diagnostics.DebuggerDisplay("Length={Length}")]
public class LinkedList<T>
{
LList<T>.Cell head;
// ....
public void Prepend(T item)
{
LList<T>.Cell oldHead;
LList<T>.Cell newHead;
do
{
oldHead = head;
newHead = LList<T>.Cons(item, oldHead);
} while (!Object.ReferenceEquals(Interlocked.CompareExchange(ref head, newHead, oldHead), oldHead));
}
// ....
}
现在Prepend
成功后,读取head
的帖子是否保证获得最新版本,即使它未标记为volatile
?
我一直在做一些实证测试,它似乎工作得很好,我在这里搜索过但没有找到一个明确的答案(一堆不同的问题和评论/答案都说是冲突的事情)。
答案 0 :(得分:6)
Interlocked是否保证在CompareExchange操作成功后读取MyObject的任何线程都会立即获得新值,或者我是否必须将MyObject标记为volatile来确保这一点?
是的,后续读取在同一个帖子上将获得新值。
你的循环展开到这个:
oldHead = head;
newHead = ... ;
Interlocked.CompareExchange(ref head, newHead, oldHead) // full fence
oldHead = head; // this read cannot move before the fence
修改强>:
正常缓存可能发生在其他线程上。考虑:
var copy = head;
while ( copy == head )
{
}
如果你在另一个线程上运行它,编译器可以缓存head
的值,并且永远不会看到更新。
答案 1 :(得分:4)
您的代码应该可以正常运行。虽然没有明确记录,但Interlocked.CompareExchange
方法会产生全栅栏障碍。我想你可以做一个小改动,省略Object.ReferenceEquals
调用,转而依赖!=
运算符,它默认会执行引用相等。
值得一提的是InterlockedCompareExchange Win API电话的文档要好得多。
此功能可生成完整的内存屏障(或栅栏)以确保 内存操作按顺序完成。
令人遗憾的是,在.NET BCL对应Interlocked.CompareExchange上不存在相同级别的文档,因为它很可能映射到CAS的完全相同的基础机制。
现在,在Prepend成功之后,线程读取头部是否保证 获取最新版本,即使它没有标记为易失性?
不,不一定。如果这些线程不生成获取栅栏屏障,则无法保证它们将读取最新值。确保在使用head
时执行易失性读取。您已通过Prepend
电话在Interlocked.CompareExchange
中确保了这一点。当然,该代码可以通过循环值head
进行一次循环,但是由于Interlocked
操作,下一次迭代将被刷新。
因此,如果您的问题的上下文与其他正在执行Prepend
的线程有关,那么就不需要再做任何事了。
但是如果你的问题的上下文是关于在LinkedList
上执行另一个方法的其他线程,那么请确保在适当的地方使用Thread.VolatileRead
或Interlocked.CompareExchange
。
旁注......可能会对以下代码执行微优化。
newHead = LList<T>.Cons(item, oldHead);
我看到的唯一问题是在循环的每次迭代中都分配了内存。在高争用期间,循环可能在最终成功之前旋转几次。只要在每次迭代时将链接引用重新分配给oldHead
,您就可以将此行提升到循环之外(这样您就可以获得新的读取)。这样,内存只分配一次。