引用分配是原子的,那么为什么需要 Interlocked.Exchange(ref Object, Object) ?

发布于 2024-08-19 18:49:20 字数 805 浏览 9 评论 0原文

在我的多线程 asmx Web 服务中,我有一个我自己的 SystemData 类型的类字段 _allData,它由一些标记为 volatile 的 ListDictionary 组成。系统数据 (_allData) 偶尔会刷新一次,我通过创建另一个名为 newData 的对象并用新数据填充它的数据结构来实现这一点。完成后,我只需分配

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

这应该可以工作,因为分配是原子的,并且引用旧数据的线程继续使用它,其余的线程在分配后就拥有新的系统数据。然而我的同事说,我应该使用 InterLocked.Exchange,而不是使用 volatile 关键字和简单的赋值,因为他说在某些平台上不能保证引用赋值是原子的。此外:当我将_allData字段声明为易失性时,

Interlocked.Exchange<SystemData>(ref _allData, newData); 

会产生警告“对易失性字段的引用不会被视为易失性”我应该对此有何看法?

In my multithreaded asmx web service I had a class field _allData of my own type SystemData which consists of few List<T> and Dictionary<T> marked as volatile. The system data (_allData) is refreshed once in a while and I do it by creating another object called newData and fill it's data structures with new data. When it's done I just assign

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

This should work since the assignment is atomic and the threads that have the reference to old data keep using it and the rest have the new system data just after assignment. However my collegue said that instead of using volatile keyword and simple assigment I should use InterLocked.Exchange because he said that on some platforms it's not guaranteed that reference assignment is atomic. Moreover: when I declare the _allData field as volatile the

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produces warning "a reference to a volatile field will not be treated as volatile" What should I think about this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

爱她像谁 2024-08-26 18:49:20

这里有很多问题。一次考虑一个:

引用分配是原子的,那么为什么需要Interlocked.Exchange(ref Object, Object)?

引用分配是原子的。 Interlocked.Exchange 不仅仅进行引用分配。它读取变量的当前值,隐藏旧值,并将新值分配给变量,所有这些都是原子操作。

我的同事说,在某些平台上,不能保证引用分配是原子的。我的同事说得对吗?

不会。在所有 .NET 平台上,引用分配保证是原子的。

我的同事是根据错误的前提进行推理的。这是否意味着他们的结论是错误的?

未必。你的同事可能会出于不好的原因给你提供好的建议。也许还有其他一些原因说明您应该使用 Interlocked.Exchange。无锁编程非常困难,一旦你偏离了该领域专家所拥护的既定实践,你就陷入了困境,并面临着最糟糕的竞争条件的风险。我既不是这个领域的专家,也不是你的代码的专家,所以我无法以某种方式做出判断。

产生警告“对易失性字段的引用不会被视为易失性”我应该对此有何看法?

您应该了解为什么这是一个普遍问题。这将有助于理解为什么警告在这种特殊情况下并不重要。

编译器给出此警告的原因是因为将字段标记为 易失性意味着“该字段将在多个线程上更新 - 不要生成任何缓存该字段值的代码,并确保任何读取或写入该字段不会因处理器缓存不一致而“在时间上前后移动”。

(我假设您已经了解了所有这些。如果您没有详细了解 易失性 的含义以及它如何影响处理器缓存语义,那么您就不了解它是如何工作的,并且不应该使用易失性。无锁程序很难做到正确;确保您的程序是正确的,因为您了解它是如何工作的,而不是偶然正确的。)

现在假设您通过将引用传递给该字段来创建一个变量,该变量是该字段的别名。在被调用的方法内部,编译器没有任何理由知道引用需要具有易失性语义!编译器会很高兴地为无法实现易失性字段规则的方法生成代码,但变量一个易失性字段。这会完全破坏你的无锁逻辑;假设始终是易失性字段总是使用易失性语义进行访问。有时将其视为不稳定的,而有时将其视为不稳定的,这是没有意义的;您必须始终保持一致,否则您无法保证其他访问的一致性。

因此,当你这样做时,编译器会发出警告,因为它可能会完全搞乱你精心开发的无锁逻辑。

当然,Interlocked.Exchange 的编写目的是期望一个易失性字段并执行正确的操作。因此,该警告具有误导性。对此我感到非常遗憾;我们应该做的是实现某种机制,通过这种机制,像 Interlocked.Exchange 这样的方法的作者可以在该方法上放置一个属性,说明“此采用引用的方法对变量强制执行易失性语义,因此抑制警告”。也许在编译器的未来版本中我们会这样做。

There are numerous questions here. Considering them one at a time:

reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?

Reference assignment is atomic. Interlocked.Exchange does not do only reference assignment. It does a read of the current value of a variable, stashes away the old value, and assigns the new value to the variable, all as an atomic operation.

my colleague said that on some platforms it's not guaranteed that reference assignment is atomic. Was my colleague correct?

No. Reference assignment is guaranteed to be atomic on all .NET platforms.

My colleague is reasoning from false premises. Does that mean that their conclusions are incorrect?

Not necessarily. Your colleague could be giving you good advice for bad reasons. Perhaps there is some other reason why you ought to be using Interlocked.Exchange. Lock-free programming is insanely difficult and the moment you depart from well-established practices espoused by experts in the field, you are off in the weeds and risking the worst kind of race conditions. I am neither an expert in this field nor an expert on your code, so I cannot make a judgement one way or the other.

produces warning "a reference to a volatile field will not be treated as volatile" What should I think about this?

You should understand why this is a problem in general. That will lead to an understanding of why the warning is unimportant in this particular case.

The reason that the compiler gives this warning is because marking a field as volatile means "this field is going to be updated on multiple threads -- do not generate any code that caches values of this field, and make sure that any reads or writes of this field are not "moved forwards and backwards in time" via processor cache inconsistencies."

(I assume that you understand all that already. If you do not have a detailed understanding of the meaning of volatile and how it impacts processor cache semantics then you don't understand how it works and should not be using volatile. Lock-free programs are very difficult to get right; make sure that your program is right because you understand how it works, not right by accident.)

Now suppose you make a variable which is an alias of a volatile field by passing a ref to that field. Inside the called method, the compiler has no reason whatsoever to know that the reference needs to have volatile semantics! The compiler will cheerfully generate code for the method that fails to implement the rules for volatile fields, but the variable is a volatile field. That can completely wreck your lock-free logic; the assumption is always that a volatile field is always accessed with volatile semantics. It makes no sense to treat it as volatile sometimes and not other times; you have to always be consistent otherwise you cannot guarantee consistency on other accesses.

Therefore, the compiler warns when you do this, because it is probably going to completely mess up your carefully developed lock-free logic.

Of course, Interlocked.Exchange is written to expect a volatile field and do the right thing. The warning is therefore misleading. I regret this very much; what we should have done is implement some mechanism whereby an author of a method like Interlocked.Exchange could put an attribute on the method saying "this method which takes a ref enforces volatile semantics on the variable, so suppress the warning". Perhaps in a future version of the compiler we shall do so.

青巷忧颜 2024-08-26 18:49:20

要么你的同事错了,要么他知道 C# 语言规范所不知道的东西。

变量引用的原子性

“读取和写入以下内容
数据类型应是原子的:bool、char、
字节、sbyte、短、ushort、uint、int、
浮点数和引用类型。”

因此,您可以写入易失性引用,而不会有获得损坏值的风险。

当然,您应该小心如何决定哪个线程应该获取新数据,以最大程度地减少多个线程获取新数据的风险一次线程就可以做到这一点。

Either your collegue is mistaken, or he knows something that the C# language specification doesn't.

Atomicity of variable references:

"Reads and writes of the following
data types shall be atomic: bool, char,
byte, sbyte, short, ushort, uint, int,
float, and reference types."

So, you can write to the volatile reference without risk of getting a corrupted value.

You should of course be careful with how you decide which thread should fetch the new data, to minimise the risk that more than one thread at a time does that.

三月梨花 2024-08-26 18:49:20

Interlocked.Exchange< T>

将指定类型 T 的变量设置为指定值并返回原始值,作为原子操作。

它改变并返回原始值,它没有用,因为你只想改变它,正如 Guffa 所说,它已经是原子的了。

除非探查器证明它是应用程序中的瓶颈,否则您应该考虑使用锁,因为它更容易理解并证明您的代码是正确的。

Interlocked.Exchange< T >

Sets a variable of the specified type T to a specified value and returns the original value, as an atomic operation.

It changes and returns the original value, it's useless because you only want to change it and, as Guffa said, it's already atomic.

Unless a profiler as proven it to be a bottleneck in your application, you should consider using locks, as it's easier to understand and to prove that your code is right.

古镇旧梦 2024-08-26 18:49:20

Interlocked.Exchange() 不仅仅是原子性的,它还负责内存可见性:

以下同步函数使用适当的屏障来确保内存排序:

进入或离开临界区的函数

向同步对象发出信号的函数

等待函数

互锁功能

同步和多处理器问题

这意味着除了原子性之外,它还确保:

  • 对于调用它的线程:
    • 不会对指令进行重新排序(通过编译器、运行时或硬件)。
  • 对于所有线程:
    • 在此指令之前从内存中进行的任何读取都不会看到该指令之后发生的内存更改(通过调用该指令的线程)。这听起来似乎很明显,但缓存行可能会刷新到主内存,而不是按照写入的顺序。
    • 此指令之后的所有读取都将看到此指令所做的更改以及此指令之前(由调用此指令的线程)所做的所有更改。
    • 此指令之后对内存的所有写入都将在该指令更改到达主内存后发生(通过在完成时将该指令更改刷新到主内存,并且不让硬件按时刷新它自己)。

Interlocked.Exchange() is not just atomic, it also takes care of memory visibility:

The following synchronization functions use the appropriate barriers to ensure memory ordering:

Functions that enter or leave critical sections

Functions that signal synchronization objects

Wait functions

Interlocked functions

Synchronization and Multiprocessor Issues

This means that in addition to atomicity it ensures that:

  • For the thread calling it:
    • No reordering of the instructions is done (by the compiler, the run-time or the hardware).
  • For all threads:
    • No reads from memory before this instruction will see changes to memory (by the thread that called this instruction) that happened after this instruction. This may sound obvious but cache lines may be flushed to main memory not in the order they were written to.
    • All reads after this instruction will see the change made by this instruction and all changes made (by the thread that called this instruction) before this instruction.
    • All writes to memory after this instruction will happen after this instruction change has reached the main memory (by flushing this instruction change to main memory when its done and not let the hardware flush it own its on timing).
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文