并发:缓存一致性问题或编译器优化



根据我的理解,如果硬件在多处理器系统上支持缓存一致性,那么对共享变量的写入将对其他处理器上运行的线程可见。为了测试这一点,我用Java和pThreads编写了一个简单的程序来测试这个

public class mainTest {
public static int i=1, j = 0;
public static void main(String[] args) {
/*
* Thread1: Sleeps for 30ms and then sets i to 1
*/
(new Thread(){
public void run(){
synchronized (this) {
try{
Thread.sleep(30);
System.out.println("Thread1: j=" + mainTest.j);
mainTest.i=0;
}catch(Exception e){
throw new RuntimeException("Thread1 Error");
}
}
}
}).start();
/*
* Thread2: Loops until i=1 and then exits.
*/
(new Thread(){
public void run(){
synchronized (this) {
while(mainTest.i==1){
//System.out.println("Thread2: i = " + i); Comment1
mainTest.j++;
}
System.out.println("nThread2: i!=1, j=" + j);
}
}
}).start();
/*
*  Sleep the main thread for 30 seconds, instead of using join. 
*/
Thread.sleep(30000);
}
}


/* pThreads */
#include<stdio.h>
#include<pthread.h>
#include<assert.h>
#include<time.h>
int i = 1, j = 0;
void * threadFunc1(void * args) {
sleep(1);
printf("Thread1: j = %dn",j);
i = 0;
}
void * threadFunc2(void * args) {
while(i == 1) {
//printf("Thread2: i = %dn", i);
j++;
}
}
int main() {
pthread_t t1, t2;
int res;
printf("Main: creating threadsn");
res = pthread_create(&t1, NULL, threadFunc1, "Thread1"); assert(res==0);
res = pthread_create(&t2, NULL, threadFunc2, "Thread2"); assert(res==0);
res = pthread_join(t1,NULL); assert(res==0);
res = pthread_join(t2,NULL); assert(res==0);
printf("i = %dn", i);
printf("Main: Endn");
return 0;
}    

我注意到pThread程序总是结束。(我针对 thread1 的不同睡眠时间对其进行了测试)。然而,Java程序只结束了很少的次数;大多数时候都没有结束。 如果我在 java 程序中取消注释 Comment1,那么它会一直结束。此外,如果我使用可变性,那么在所有情况下它都会结束于 java。

所以我的困惑是,

  1. 如果缓存一致性是在硬件中完成的,那么"i=0"应该对其他线程可见,除非 编译器优化了代码。但是,如果编译器优化了代码,那么我不明白为什么线程有时会结束,有时不会。此外,添加System.out.println似乎可以改变行为。

  2. 任何人都可以看到Java所做的编译器优化(不是由C编译器完成的),这是导致这种行为

    的吗?
  3. 编译器是否还必须执行其他操作,即使硬件已经支持缓存一致性,也可以获得缓存一致性?(如启用/禁用)

  4. 默认情况下,我应该对所有共享变量使用 Volatile 吗?

我错过了什么吗?欢迎任何其他评论。

如果缓存一致性是在硬件中完成的,那么"i=0"应该对其他线程可见,除非编译器优化了代码。但是,如果编译器优化了代码,那么我不明白为什么线程有时会结束,有时不会。此外,添加System.out.println似乎可以改变行为。

注意:javac几乎没有优化,因此不要考虑静态优化。

您正在锁定与要修改的对象无关的不同对象。 由于您正在修改的字段不是volatile因此 JVM 优化器可以自由地根据自己的选择动态优化它,而不管您的硬件可以提供何种支持。

由于这是动态的,因此它可能会也可能不会优化您在该线程中未更改的字段的读取。

任何人都可以看到Java所做的编译器优化(不是由C编译器完成的),这是导致这种行为

的吗?

优化很可能是读取缓存在寄存器中或完全消除代码。此优化通常需要大约 10-30 毫秒,因此您要测试此优化是否已在程序完成之前进行。

编译器是否还必须执行其他操作,即使硬件已经支持缓存一致性,也可以获得缓存一致性?(如启用/禁用)

您必须正确使用该模型,忘记编译器将优化代码的想法,理想情况下使用并发库在线程之间传递工作。

public static void main(String... args) {
final AtomicBoolean flag = new AtomicBoolean(true);
/*
* Thread1: Sleeps for 30ms and then sets i to 1
*/
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(30);
System.out.println("Thread1: flag=" + flag);
flag.set(false);
} catch (Exception e) {
throw new RuntimeException("Thread1 Error");
}
}
}).start();
/*
* Thread2: Loops until flag is false and then exits.
*/
new Thread(new Runnable() {
@Override
public void run() {
long j = 0;
while (flag.get())
j++;
System.out.println("nThread2: flag=" + flag + ", j=" + j);
}
}).start();
}

指纹

Thread1: flag=true
Thread2: flag=false, j=39661265

默认情况下,我应该对所有共享变量使用 Volatile 吗?

几乎从不。如果您只有一个 since 标志,如果您只设置一次,它将起作用。但是,使用锁定通常更可能有用。

您的特定问题是,在第一个线程将内存设置为 0 后i,第二个线程需要同步内存。因为两个线程都在this上同步,正如@Peter和@Marko所指出的那样,这是不同的对象。 第二个线程可以进入while循环,_before第一个线程设置i = 0while循环中没有跨越额外的内存屏障,因此该字段永远不会更新。

如果我在 java 程序中取消注释 Comment1,那么它会一直结束。

这是因为底层System.outPrintStreamsynchronized,导致跨越内存屏障。 内存屏障强制线程和中央内存之间同步内存,并确保内存操作的顺序。 以下是PrintStream.println(...)来源:

public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

如果缓存一致性是在硬件中完成的,那么"i=0"应该对其他线程可见,除非编译器优化了代码

您必须记住,每个处理器都有几个寄存器和大量的每个处理器缓存。 缓存内存是这里的主要问题,而不是编译器优化。

任何人都可以看到Java所做的编译器优化(不是由C编译器完成的),这是导致这种行为

的吗?

使用缓存内存和内存操作重新排序都是重要的性能优化。 处理器可以自由更改操作顺序以改进流水线,除非越过内存屏障,否则它们不会同步其脏页。 这意味着线程可以使用本地高速内存异步运行,以[显着]提高性能。 Java 内存模型允许这样做,并且与 pthreads 相比要复杂得多。

默认情况下,我应该对所有共享变量使用 volatile 吗?

如果您希望线程 #1 更新字段,并且线程 #2 看到该更新,那么是的,您需要将该字段标记为volatile。 通常建议使用Atomic*类,如果要递增共享变量(++是两个操作),则需要

使用。如果要执行多个操作(例如遍历共享集合),则应使用synchronized关键字。

如果线程 2 在线程 1 已将 i 设置为 0 后开始运行,则程序将结束。使用synchronized(this)可能会在一定程度上促成这种情况,因为无论获取的锁如何,同步块的每个入口都有一个内存屏障(您使用不同的锁,因此不会发生争用)。

除此之外,在代码获得 JITted 的那一刻和线程 1 写入 0 的那一刻之间可能还有其他复杂的交互,因为这会改变优化级别。优化的代码通常只会从全局变量中读取一次,并将值缓存在寄存器或类似的线程本地位置。

缓存一致性是一项硬件级别的功能。操作变量如何映射到 CPU 指令并间接映射到硬件是一种语言/运行时功能。

换句话说,设置变量不一定转换为写入该变量内存的 CPU 指令。编译器(脱机或 JIT)可以使用其他信息来确定它不需要写入内存。

话虽如此,大多数支持并发的语言都有额外的语法来告诉编译器您正在使用的数据用于并发访问。对于许多人(如Java)来说,这是选择加入。

如果预期行为是线程 2 检测变量的变化并终止,则绝对需要"volatile"关键字。它允许 thead 能够通过易失变量进行通信。编译器通常优化为从缓存中获取,因为与从主内存中获取相比,它更快。

看看这个很棒的帖子,它会给你答案: http://jeremymanson.blogspot.sg/2008/11/what-volatile-means-in-java.html

我相信在这种情况下,它与缓存一致性无关。如前所述,它是一个计算机体系结构功能,对 c/java 程序应该是透明的。 如果未指定 volatile ,则行为是未定义的,这就是为什么有时另一个线程可以获取值更改而有时不能。

在 C 和 Java 上下文中易失性具有不同的含义。 http://en.wikipedia.org/wiki/Volatile_variable

根据您的 C 编译器,该程序可能会得到优化并具有与 Java 程序相同的效果。因此,始终建议使用易变关键字。

最新更新