将complete
设为volatile有两个作用:
让我们先考虑第一个。JIT完全有权利看到循环体:
while(!complete) toggle = !toggle
不修改complete
,因此循环开始时complete
的任何值都将永远保持不变。 因此,Jitter可以生成代码,就好像您编写了:
if (!complete) while(true) toggle = !toggle
或者更有可能:
bool local = complete;
while(local) toggle = !toggle;
让
complete
成为volatile可以防止两种优化。
但是你想要的是volatile的第二个效果。假设你的两个线程在不同的处理器上运行。每个处理器都有自己的处理器缓存,这是主内存的副本。假设这两个处理器都复制了一个
complete
为false的主内存。当一个处理器的缓存将
complete
设置为true时,如果
complete
不是volatile,那么“切换”处理器不需要注意到这一点;它有自己的缓存,在其中
complete
仍然为false,每次回到主内存会很昂贵。
将
complete
标记为volatile消除了此优化。如何消除是处理器的实现细节。也许在每次volatile写入时,写入都被写入主内存,每个其他处理器都丢弃其缓存。或者可能有其他策略。处理器选择如何进行操作取决于制造商。
关键是,任何时候使字段成为volatile并读取或写入它,您都会大大干扰编译器、JIT和处理器优化代码的能力。首先尝试不使用volatile字段;使用更高级别的构造,并且不要在线程之间共享内存。
我正在尝试将句子“acquire-fence防止其他读/写被移动到fence之前…”可视化。该指令之前不应该有什么内容?
考虑指令可能是无益的。与其考虑一堆“指令”,不如只专注于读写的顺序。其他都是无关紧要的。
假设您有一个内存块,并且其中一部分被复制到了两个缓存中。出于性能原因,您主要从缓存中读取和写入。不时地,您会将缓存与主内存重新同步。这对读写序列有什么影响?
假设我们希望对单个整数变量执行此操作:
1. 处理器Alpha将0写入主内存。
2. 处理器Bravo从主内存读取0。
3. 处理器Bravo将1写入主内存。
4. 处理器Alpha从主内存读取1。
假设实际发生的是:
- 处理器Alpha向缓存中写入0,并与主内存同步。
- 处理器Bravo从主内存同步缓存并读取0。
- 处理器Bravo向缓存中写入1,并将缓存与主内存同步。
- 处理器Alpha从其缓存中读取0 -- 即旧值。
这究竟有什么不同呢?
- 处理器Alpha将0写入主内存。
- 处理器Bravo从主内存读取0。
- 处理器Alpha从主内存读取0。
- 处理器Bravo将1写入主内存。
它们没有区别。缓存可以将"写-读-写-读"转化为"写-读-读-写"。它将一个读操作向后移动,在这种情况下,等价地将一个写操作向前移动。
这个例子只涉及对一个位置进行的两次读写操作,但是您可以想象一种场景,其中对许多位置进行了许多读写操作。处理器可以广泛地将读操作向后移动,并将写操作向前移动。关于哪些操作是合法的以及哪些操作是不合法的具体规则因处理器而异。
一个栅栏是一种屏障,阻止读取向后移动或写入向前越过它。所以如果我们有:
- 处理器 Alpha 将 0 写入主内存。
- 处理器 Bravo 从主内存读取 0。
- 处理器 Bravo 将 1 写入主内存。放置栅栏。
- 处理器 Alpha 从主内存读取 1。
无论处理器使用什么缓存策略,现在都不允许将读取4移动到栅栏之前的任何点。同样,也不允许将写入3向后移动到栅栏之后的任何点。处理器如何实现栅栏由其自己决定。
volatile
或任何内存屏障生成器时,您正在告诉所有层约束指令移动优化。所以回答你的问题...是的,volatile
关键字告诉Jitter防止“提升”优化。读取线程上的获取栅栏是编译器停止优化的通知! - Brian Gideon