9

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

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

但是阅读呢?是否Interlocked保证操作成功MyObject后读取的任何线程CompareExchange都会立即获得新值,还是我必须标记MyObjectvolatile确保这一点?

我想知道的原因是我已经实现了一个无锁链表,当你向它添加一个元素时,它会不断更新自身内部的“头”节点,如下所示:

[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

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

4

2 回答 2

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并且永远不会看到更新。

于 2011-12-06T12:18:10.720 回答
5

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

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

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

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

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

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

因此,如果您的问题的上下文是关于也在执行的其他线程,Prepend那么不需要做更多的事情。

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

旁注......可能有一个可以对以下代码执行的微优化。

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

我看到的唯一问题是在循环的每次迭代中都分配了内存。在高竞争期间,循环可能会在最终成功之前旋转数次。只要您oldHead在每次迭代中重新分配链接引用(以便您获得新的阅读),您就可以将这条线提升到循环之外。这种方式内存只分配一次。

于 2011-12-06T19:57:00.863 回答