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

时间:2022-02-10 21:01:31

There are a fair share of questions about Interlocked vs. volatile here on SO, I understand and know the concepts of volatile (no re-ordering, always reading from memory, etc.) and I am aware of how Interlocked works in that it performs an atomic operation.

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

But my question is this: Assume I have a field that is read from several threads, which is some reference type, say: public Object MyObject;. I know that if I do a compare exchange on it, like this: Interlocked.CompareExchange(ref MyObject, newValue, oldValue) that interlocked guarantees to only write newValue to the memory location that ref MyObject refers to, if ref MyObject and oldValue currently refer to the same object.

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

But what about reading? Does Interlocked guarantee that any threads reading MyObject after the CompareExchange operation succeeded will instantly get the new value, or do I have to mark MyObject as volatile to ensure this?

但阅读呢? Interlocked是否保证在CompareExchange操作成功后读取MyObject的任何线程将立即获得新值,或者我是否必须将MyObject标记为volatile来确保这一点?

The reason I'm wondering is that I've implemented a lock-free linked list which updates the "head" node inside itself constantly when you prepend an element to it, like this:

我想知道的原因是我已经实现了一个无锁链接列表,当你为它添加一个元素时,它会不断更新其内部的“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));
    }

    // ....
}

Now after Prepend succeeds, are the threads reading head guaranteed to get the latest version, even though it's not marked as volatile?

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

I have been doing some empirical tests, and it seems to be working fine, and I have searched here on SO but not found a definitive answer (a bunch of different questions and comments/answer in them all say conflicting things).

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

2 个解决方案

#1


4  

Your code should work fine. Though it is not clearly documented the Interlocked.CompareExchange method will produce a full-fence barrier. I suppose you could make one small change and omit the Object.ReferenceEquals call in favor of relying on the != operator which would perform reference equality by default.

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

For what it is worth the documentation for the InterlockedCompareExchange Win API call is much better.

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

This function generates a full memory barrier (or fence) to ensure that memory operations are completed in order.

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

It is a shame the same level documenation does not exist on the .NET BCL counterpart Interlocked.CompareExchange because it is very likely they map to the exact same underlying mechanisms for the CAS.

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

Now after Prepend succeeds, are the threads reading head guaranteed to get the latest version, even though it's not marked as volatile?

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

No, not necessarily. If those threads do not generate an acquire-fence barrier then there is no guarantee that they will read the latest value. Make sure that you perform a volatile read upon any use of head. You have already ensured that in Prepend with the Interlocked.CompareExchange call. Sure, that code may go through the loop once with an stale value of head, but the next iteration will be refreshed due to the Interlocked operation.

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

So if the context of your question was in regards to other threads who are also executing Prepend then nothing more needs to be done.

因此,如果您的问题的上下文与其他正在执行Prepend的线程相关,则无需再进行任何操作。

But if the context of your question was in regards to other threads executing another method on LinkedList then make sure you use Thread.VolatileRead or Interlocked.CompareExchange where appropriate.

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

Side note...there may be a micro-optimization that could be performed on the following code.

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

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

The only problem I see with this is that memory is allocated on every iteration of the loop. During periods of high contention the loop may spin around several times before it finally succeeds. You could probably lift this line outside of the loop as long as you reassign the linked reference to oldHead on every iteration (so that you get the fresh read). This way memory is only allocated once.

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

#2


6  

Does Interlocked guarantee that any threads reading MyObject after the CompareExchange operation succeeded will instantly get the new value, or do I have to mark MyObject as volatile to ensure this?

Interlocked是否保证在CompareExchange操作成功后读取MyObject的任何线程将立即获得新值,或者我是否必须将MyObject标记为volatile来确保这一点?

Yes, subsequent reads on the same thread will get the new value.

是的,在同一个线程上的后续读取将获得新值。

Your loop unrolls to this:

你的循环展开到这个:

oldHead = head;
newHead = ... ;

Interlocked.CompareExchange(ref head, newHead, oldHead) // full fence

oldHead = head; // this read cannot move before the fence

EDIT:

编辑:

Normal caching can happen on other threads. Consider:

正常缓存可能发生在其他线程上。考虑:

var copy = head;

while ( copy == head )
{
}

If you run that on another thread, the complier can cache the value of head and never see the update.

如果你在另一个线程上运行它,编译器可以缓存head的值,永远不会看到更新。

#1


4  

Your code should work fine. Though it is not clearly documented the Interlocked.CompareExchange method will produce a full-fence barrier. I suppose you could make one small change and omit the Object.ReferenceEquals call in favor of relying on the != operator which would perform reference equality by default.

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

For what it is worth the documentation for the InterlockedCompareExchange Win API call is much better.

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

This function generates a full memory barrier (or fence) to ensure that memory operations are completed in order.

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

It is a shame the same level documenation does not exist on the .NET BCL counterpart Interlocked.CompareExchange because it is very likely they map to the exact same underlying mechanisms for the CAS.

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

Now after Prepend succeeds, are the threads reading head guaranteed to get the latest version, even though it's not marked as volatile?

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

No, not necessarily. If those threads do not generate an acquire-fence barrier then there is no guarantee that they will read the latest value. Make sure that you perform a volatile read upon any use of head. You have already ensured that in Prepend with the Interlocked.CompareExchange call. Sure, that code may go through the loop once with an stale value of head, but the next iteration will be refreshed due to the Interlocked operation.

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

So if the context of your question was in regards to other threads who are also executing Prepend then nothing more needs to be done.

因此,如果您的问题的上下文与其他正在执行Prepend的线程相关,则无需再进行任何操作。

But if the context of your question was in regards to other threads executing another method on LinkedList then make sure you use Thread.VolatileRead or Interlocked.CompareExchange where appropriate.

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

Side note...there may be a micro-optimization that could be performed on the following code.

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

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

The only problem I see with this is that memory is allocated on every iteration of the loop. During periods of high contention the loop may spin around several times before it finally succeeds. You could probably lift this line outside of the loop as long as you reassign the linked reference to oldHead on every iteration (so that you get the fresh read). This way memory is only allocated once.

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

#2


6  

Does Interlocked guarantee that any threads reading MyObject after the CompareExchange operation succeeded will instantly get the new value, or do I have to mark MyObject as volatile to ensure this?

Interlocked是否保证在CompareExchange操作成功后读取MyObject的任何线程将立即获得新值,或者我是否必须将MyObject标记为volatile来确保这一点?

Yes, subsequent reads on the same thread will get the new value.

是的,在同一个线程上的后续读取将获得新值。

Your loop unrolls to this:

你的循环展开到这个:

oldHead = head;
newHead = ... ;

Interlocked.CompareExchange(ref head, newHead, oldHead) // full fence

oldHead = head; // this read cannot move before the fence

EDIT:

编辑:

Normal caching can happen on other threads. Consider:

正常缓存可能发生在其他线程上。考虑:

var copy = head;

while ( copy == head )
{
}

If you run that on another thread, the complier can cache the value of head and never see the update.

如果你在另一个线程上运行它,编译器可以缓存head的值,永远不会看到更新。