C# 内存模型和非易失性变量在其他线程创建之前初始化
我有一个与 C# 内存模型和线程相关的问题。我不确定以下代码在没有 volatile 关键字的情况下是否正确。
public class A {
private int variableA = 0;
public A() {
variableA = 1;
Thread B = new Thread(new ThreadStart(() => printA())).Start();
}
private void printA() {
System.Console.WriteLine(variableA);
}
}
我担心的是,是否可以保证线程 B 在不使用易失性的情况下看到值为 1 的变量 A?在主线程中,我只在构造函数中将 1 分配给变量 A。之后我不再接触变量A,它仅在线程B中使用,因此锁定可能不是必需的。
但是,是否可以保证主线程将刷新其缓存并将variableA内容写入主内存,以便第二个线程可以读取新分配的值?
另外,是否保证第二个线程将从主存中读取variableA的内容?是否可能发生一些编译器优化,线程 B 可以从缓存而不是主内存中读取变量 A 的内容?当指令的顺序改变时,可能会发生这种情况。
当然,将易失性添加到variableA声明将使代码正确。但是,有必要吗?我这样问是因为我在构造函数中编写了一些带有一些非易失性变量初始化的代码,并且这些变量稍后被一些计时器线程使用,我不确定它是否完全正确。
同样的代码在 Java 中怎么样?
谢谢,米哈尔
I have a question related to the C# memory model and threads. I am not sure if the following code is correct without the volatile keyword.
public class A {
private int variableA = 0;
public A() {
variableA = 1;
Thread B = new Thread(new ThreadStart(() => printA())).Start();
}
private void printA() {
System.Console.WriteLine(variableA);
}
}
My concern is if it is guaranteed that the Thread B will see variableA with value 1 without using volatile? In the main thread I am only assigning 1 to variableA in the constructor. After that I am not touching variableA, it is used only in the Thread B, so locking is probably not necessary.
But, is it guaranteed that the main thread will flush his cache and write the variableA contents to the main memory, so the second thread can read the newly assigned value?
Additionally, is it guaranteed that the second thread will read the variableA contents from the main memory? May some compiler optimizations occur and the Thread B can read the variableA contents from the cache instead of the main memory? It may happen when the order of the instructions is changed.
For sure, adding volatile to the variableA declaration will make the code correct. But, is it neccessary? I am asking because I wrote some code with some non volatile variables initialization in the constructor, and the variables are used later by some Timer threads, and I am not sure if it is totally correct.
What about the same code in Java?
Thanks, Michal
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
有很多地方会产生隐式记忆障碍。这是其中之一。启动线程会创建完整的屏障。因此,对
variableA
的写入将在线程启动之前提交,并且将从主内存获取第一次读取。当然,在 Microsoft 的 CLR 实现中,这有点没有实际意义,因为写入已经具有易失性语义。但 ECMA 规范中没有提供相同的保证,因此从理论上讲,Mono 实现在这方面可能会有不同的行为。在这种情况下……是的。但是,如果您继续在第二个线程中使用
variableA
,则在第一次读取后无法保证它将看到更新。是的。
是的,但仅限于第一次阅读时。
在这个非常具体和狭窄的情况下......不。但是,一般来说,建议您在这些场景中使用
volatile
关键字。当场景变得更加复杂时,它不仅会使您的代码线程安全,而且还有助于记录该字段将被多个线程使用这一事实,并且您已经考虑了使用锁的含义 -自由策略。There are a lot of places where implicit memory barriers are created. This is one of them. Starting threads create full barriers. So the write to
variableA
will get committed before the thread starts and the first reads will be acquired from main memory. Of course, in Microsoft's implementation of the CLR that is somewhat of a moot point because writes already have volatile semantics. But the same guarentee is not made in the ECMA specification so it is theorectically possible that the Mono implemenation could behave differently in this regard.In this case...yes. However, if you continue to use
variableA
in the second thread there is no guarentee after the first read that it will see updates.Yes.
Yes, but only on the first read.
In this very specific and narrow case...no. But, in general it is advised that you use the
volatile
keyword in these scenarios. Not only will it make your code thread-safe as the scenario gets more complicated, but it also helps to document the fact that the field is going to be used by more than one thread and that you have considered the implications of using a lock-free strategy.Java 中的相同代码绝对没问题——新线程的创建实际上充当了一种屏障。 (程序文本中早于线程创建的所有操作都“发生在”新线程启动之前。)
但是,我不知道 .NET 中对于新线程创建有何保证。更令人担忧的是使用 Control.BeginInvoke 等时可能会出现延迟读取......我还没有看到针对这些情况的内存屏障的任何保证。
说实话,我怀疑没问题。我怀疑任何需要在像这样的线程之间进行协调的事情(无论是创建一个新线程还是编组对现有线程的调用)都将在所涉及的两个线程上使用完整的内存屏障。然而,你的担心是绝对正确的,我希望你能从比我更聪明的人那里得到更明确的答案。您可能想给 Joe Duffy 发电子邮件以了解他对此的看法......
The same code in Java is definitely okay - the creation of a new thread acts as a sort of barrier, effectively. (All actions earlier in the program text than the thread creation "happen before" the new thread starts.)
I don't know what's guaranteed in .NET with respect to new thread creation, however. Even more worrying is the possibility of a delayed read when using
Control.BeginInvoke
and the like... I haven't seen any guarantees around memory barriers for those situations.To be honest, I suspect it's fine. I suspect that anything which needs to coordinate between threads like this (either creating a new one or marshalling a call onto an existing one) will use a full memory barrier on both of the threads involved. However, you're absolutely right to be concerned, and I'm hoping that you'll get a more definitive answer from someone smarter than me. You might want to email Joe Duffy to get his point of view on this...
是的,这是由MS CLR内存模型保证的。对于 CLI 的其他实现则不一定如此(即,我不确定 Mono)。 ECMA 标准不要求它。
这需要刷新缓存。它可能是通过线程的创建来保证的(就像 Jon Skeet 所说)。然而,前一点并不能保证这一点。缓存会在每次写入时刷新,但不会在每次读取时刷新。
您可以使用
VolatileRead(ref variableA)
来确保这一点,但建议 (Jeffrey Richter) 使用Interlocked
类。请注意,VolatileWrite()
在 MS.NET 中是多余的。Yes, this is guaranteed by the MS CLR memory model. Not necessarily so for other implementations of the CLI (ie, I'm not sure about Mono). The ECMA standard does not require it.
That requires that the cache has been refreshed. It is probably guaranteed by the creation of the Thread (like Jon Skeet said). It is however not guaranteed by the previous point. The cache is flushed on each write but not on each read.
You could make very sure by using
VolatileRead(ref variableA)
but it is recommended (Jeffrey Richter) to use theInterlocked
class. Note thatVolatileWrite()
is superfluous in MS.NET.