繁体   English   中英

字段由多个线程读取/写入,Interlocked与volatile

[英]Fields read from/written by several threads, Interlocked vs. volatile

在SO上有很多关于Interlocked vs. volatile的问题,我理解并且知道volatile的概念(没有重新排序,总是从内存中读取等)并且我知道Interlocked工作方式是它执行的原子操作。

但我的问题是:假设我有一个从多个线程读取的字段,这是一些引用类型,例如: public Object MyObject; 我知道,如果我对它做一个比较交换,就像这样: Interlocked.CompareExchange(ref MyObject, newValue, oldValue) ,互锁保证只将newValue写入ref MyObject引用的内存位置,如果ref MyObjectoldValue当前引用对同一个对象。

但阅读呢? Interlocked是否保证在CompareExchange操作成功后读取MyObject任何线程将立即获得新值,或者我是否必须将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

我一直在做一些实证测试,它似乎工作得很好,我在这里搜索过但没有找到明确的答案(一堆不同的问题和评论/答案都说是相互矛盾的事情)。

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的值,永远不会看到更新。

你的代码应该工作正常。 虽然没有明确记录, Interlocked.CompareExchange方法将产生一个全栅栏屏障。 我想你可以做一个小改动,省略Object.ReferenceEquals调用,转而依赖于!=运算符,默认情况下会执行引用相等。

值得一提的是, InterlockedCompareExchange Win API调用的文档要好得多。

此函数生成完整的内存屏障(或栅栏),以确保按顺序完成内存操作。

遗憾的是,.NET BCL对应的Interlocked.CompareExchange上不存在相同级别的文档,因为它很可能映射到CAS的完全相同的底层机制。

现在Prepend成功之后,线程读取头是否保证获得最新版本,即使它没有标记为volatile?

不,不一定。 如果这些线程不生成获取栅栏屏障,则无法保证它们将读取最新值。 确保在使用head时执行易失性读取。 您已使用Interlocked.CompareExchange调用确保在Prepend 当然,该代码可能会以过时的head值经过循环一次,但由于Interlocked操作,下一次迭代将被刷新。

因此,如果您的问题的上下文与其他正在执行Prepend线程相关,那么就不需要做任何其他事情了。

但是,如果您的问题的上下文是关于在LinkedList上执行另一个方法的其他线程,那么请确保在适当的地方使用Thread.VolatileReadInterlocked.CompareExchange

旁注......可能会对以下代码执行微优化。

newHead = LList<T>.Cons(item, oldHead);

我看到的唯一问题是内存是在循环的每次迭代中分配的。 在高争用期间,循环可能在最终成功之前旋转几次。 只要在每次迭代时将链接引用重新分配给oldHead ,您就可以将此行提升到循环之外(这样您就可以获得新的读取)。 这样,内存只分配一次。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM