1

私の理解では、ハードウェアがマルチプロセッサ システムでキャッシュ コヒーレンスをサポートしている場合、共有変数への書き込みは、他のプロセッサで実行されているスレッドに表示されます。これをテストするために、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 = %d\n",j);
    i = 0;
}

void * threadFunc2(void * args) {
while(i == 1) {
        //printf("Thread2: i = %d\n", i);
        j++;
    }
}

int main() {
    pthread_t t1, t2;
    int res;
    printf("Main: creating threads\n");

    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 = %d\n", i);
    printf("Main: End\n");
    return 0;
}    

pThread プログラムが常に終了することに気付きました。(スレッド1のさまざまなスリープ時間でテストしました)。ただし、Java プログラムが終了するのはごくわずかです。終わらないことがほとんどです。Java プログラムで Comment1 のコメントを外すと、常に終了します。また、揮発性を使用すると、すべての場合でJavaで終了します。

だから私の混乱は、

  1. キャッシュの一貫性がハードウェアで行われる場合、コンパイラがコードを最適化しない限り、「i=0」は他のスレッドから見えるはずです。しかし、コンパイラがコードを最適化した場合、スレッドが終了する場合と終了しない場合がある理由がわかりません。また、 System.out.println を追加すると、動作が変わるようです。

  2. この動作の原因となっている、Java が行うコンパイラの最適化 (C コンパイラでは行われない) を誰でも見ることができますか?

  3. ハードウェアが既にサポートしている場合でも、キャッシュの一貫性を確保するために、コンパイラが行う必要がある追加のことはありますか? (有効化/無効化など)

  4. デフォルトですべての共有変数に Volatile を使用する必要がありますか?

何か不足していますか?追加のコメントは大歓迎です。

4

5 に答える 5

5

キャッシュの一貫性がハードウェアで行われる場合、コンパイラがコードを最適化しない限り、「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 フラグを一度だけ設定すると機能します。ただし、一般的には、ロックを使用する方が役立つ可能性が高くなります。

于 2012-10-22T19:47:59.400 に答える
3

あなたの特定の問題はi、最初のスレッドによって 0 に設定された後、2 番目のスレッドがメモリを同期する必要があることです。this@Peterと@Markoが指摘したように、両方のスレッドが同期しているため、異なるオブジェクトです。while最初のスレッドが設定する前に、2 番目のスレッドがループに入る可能性がありますi = 0。ループ内で交差する追加のメモリ バリアがないwhileため、フィールドが更新されることはありません。

Java プログラムで Comment1 のコメントを外すと、常に終了します。

これが機能するのは、メモリバリアを超える原因となる基盤System.out PrintStreamがあるためです。メモリ バリアは、スレッドと中央メモリ間の同期メモリを強制し、メモリ操作の順序を保証します。ソースは次のとおりです。synchronizedPrintStream.println(...)

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

キャッシュの一貫性がハードウェアで行われる場合、コンパイラがコードを最適化しない限り、「i=0」は他のスレッドから見えるはずです

各プロセッサには、いくつかのレジスタとプロセッサごとの大量のキャッシュ メモリの両方があることを覚えておく必要があります。ここでの主な問題は、コンパイラの最適化ではなく、キャッシュされたメモリです。

この動作の原因となっている、Java が行うコンパイラの最適化 (C コンパイラでは行われない) を誰でも見ることができますか?

キャッシュされたメモリとメモリ操作の並べ替えを使用すると、パフォーマンスが大幅に最適化されます。プロセッサは、パイプライン処理を改善するために操作の順序を自由に変更でき、メモリ バリアを越えない限り、ダーティ ページを同期しません。これは、ローカルの高速メモリを使用してスレッドを非同期に実行し、パフォーマンスを [大幅に] 向上させることができることを意味します。Java メモリ モデルはこれを可能にしますが、pthreads に比べてはるかに複雑です。

デフォルトですべての共有変数に volatile を使用する必要がありますか?

スレッド #1 がフィールドを更新し、スレッド #2 がその更新を確認すると予想される場合は、フィールドを としてマークする必要がありますvolatile。多くの場合、クラスの使用Atomic*が推奨され、共有変数をインクリメントする場合はクラスを使用する必要があります ( ++is two operations)。

複数の操作 (共有コレクション全体の反復処理など) を実行している場合は、synchronizedキーワードを使用する必要があります。

于 2012-10-22T20:00:31.477 に答える
1

キャッシュコヒーレンシはハードウェアレベルの機能です。変数の操作がCPU命令にマップされ、ハードウェアに間接的にマップされる方法は、言語/ランタイム機能です。

つまり、変数を設定しても、必ずしもその変数のメモリに書き込むCPU命令に変換されるとは限りません。コンパイラー(オフラインまたはJIT)は、他の情報を使用して、メモリーに書き込む必要がないことを判別できます。

そうは言っても、並行性をサポートするほとんどの言語には、使用しているデータが同時アクセスを目的としていることをコンパイラーに通知するための追加の構文があります。多くの場合(Javaなど)、オプトインです。

于 2012-10-22T20:25:38.977 に答える
1

予想される動作が、スレッド 2 が変数の変更を検出して終了することである場合、間違いなく「Volatile」キーワードが必要です。揮発性変数を介してスレッドが通信できるようにします。コンパイラは通常、メイン メモリからフェッチするよりも高速であるため、キャッシュからフェッチするように最適化します。

この素晴らしい投稿をチェックしてください。答えが得られます: http://jeremymanson.blogspot.sg/2008/11/what-volatile-means-in-java.html

この場合、キャッシュの一貫性とは何の関係もないと思います。前述のように、これはコンピューター アーキテクチャの機能であり、ac/java プログラムに対して透過的である必要があります。volatile が指定されていない場合、動作は未定義です。そのため、他のスレッドが値の変更を取得できる場合と取得できない場合があります。

C と Java のコンテキストでの volatile の意味は異なります。 http://en.wikipedia.org/wiki/Volatile_variable

C コンパイラによっては、プログラムが最適化され、Java プログラムと同じ効果が得られる場合があります。したがって、volatile キーワードは常に推奨されます。

于 2012-10-23T07:10:11.467 に答える
1

スレッド 1 がすでに i を 0 に設定した後にスレッド 2 が実行を開始すると、プログラムは終了synchronized(this)します。取得されたロックに関係なく、同期ブロックへの各エントリにメモリ バリアがあるため、使用がこれに多少貢献する可能性があります (異なるロックを使用するため、競合は発生しません)。

これとは別に、コードが JIT された瞬間とスレッド 1 が 0 を書き込む瞬間との間には、他の複雑な相互作用が存在する可能性があります。これは、最適化のレベルが変化するためです。最適化されたコードは通常、グローバル var から 1 回だけ読み取り、その値をレジスタまたは同様のスレッド ローカルの場所にキャッシュします。

于 2012-10-22T19:48:42.207 に答える