[Java][C]JavaのvolatileとCのvolatile

Javaでvolatileを使う場面は限られてくるけど、マルチスレッドの処理でパフォーマンスが問題になってくるようなときに使う。
通常のロックでは処理が重くなってしまうような場合だね。

VolatileSample.java
import java.util.stream.IntStream;

class VolatileSample {

    static class Counter {
        private volatile int count = 0;

        int getCount() {
            return count;
        }

        synchronized int increment() {
            return count++;
        }
    }

    public static void main(String ... args) {
        Counter counter = new Counter();

        IntStream.rangeClosed(1, 100).parallel()
            .forEach(x -> counter.increment());

        System.out.println(counter.getCount());
    }
}
# 読み込み側が1人しかいなくて、素直にAtomicInteger使えみたいな例になってるけど。

これはvolatileフィールドの以下のような性質を使っている。
  • 変数アクセスのアトミック性
  • 変数アクセスの可視性
The Java® Language Specification, Java SE 8 Edition
8.3.1.4. volatile Fields
17.7. Non-Atomic Treatment of double and long

変数へのアクセス自体はアトミック性が保証されているけど、インクリメントのような複合操作はアトミックにはならない。 だから、上記の例では書き込み時はインクリメントしているのでsynchronizedで保護してあげなくちゃいけないけど、読み込み時は問題ない。
これがもし、countフィールドにvolatile宣言をしていないと「可視性」が保証されないことになるので、別スレッドが書き込んだ値の最新値を別スレッドが読み込む保証がなくなってしまう。

アトミック性(原子性)が保証されないってのは特にlongやdoubleの変数の場合に、変数書き込みが上位バイトのみとか下位バイトのみされている状態が他のスレッドから見えてしまう可能性があるってことだね。
可視性が保証されないってのは各スレッドが変数値のコピーをそれぞれ持っていてもいいってことだね。


。。。で、ここまではいいんだけど、Cのvolatileも同じようなもんかと思ってたら、実は違う。
Cのvolatileはアトミック性を保証していない。
そもそも、Cのvolatileはスレッドなんてものとは無関係に規定されている。

じゃあ、Cのvolatileって何のためにあったんだっけ?
Javaのところでは説明していなかったけど、volatileにはもう一つ、コンパイラによる最適化の抑止という意味合いがある。

まずは以下のコードを見てみよう。

volatile.c
#include <signal.h>
#include <string.h>

sig_atomic_t stop;

void trap(int signum) {
    stop = 1;
}

int main(void) {
    struct sigaction act;

    memset(&act, 0, sizeof(act));
    act.sa_handler = trap;
    sigaction(SIGINT, &act, NULL);

    while (!stop);

    return 0;
}

これで、SIGINTが発生した場合に無限ループを抜けられる!と思っても実は抜けられなかったりする。
$ clang -Wall -O2 -o volatile volatile.c
$ ./volatile
^C           # 止まらない。。。

じゃあ、今度はvolatileを付けてみよう。
#include <signal.h>
#include <string.h>

volatile sig_atomic_t stop;
// あとは同じ
$ clang -Wall -O2 -o volatile volatile.c
$ ./volatile
^C$          # 今度はプロンプトに戻ってきた。

今度は止まった。何が違うんだろうか。
ちょっとコンパイラが出力したアセンブラを見てみよう。注目箇所だけ抜粋

volatileあり
LBB1_1:
 cmp     dword ptr [rax], 0
        je      LBB1_1
volatileなし
        cmp     dword ptr [rax], 0
        sete    al
LBB1_1:
        test    al, 1
        mov     al, 1
        jne     LBB1_1

volatileをつけている方は素直にwhile(!stop);と同じ動作になっているけど、volatileをつけていない方は最適化によって、以下と同様の動きになってしまっている。
    if (!stop) {
        while(1);
    }

だって、while(!stop);ループの中ではstopを書き換えていないんだから、永久に抜け出せないだろ!ってコンパイラが判断したんだね。
でも、そうじゃないよ、違うどこかでstopが書き換わっちゃう可能性があるから、最適化しないでくれよ!ってコンパイラに 伝えるのためにvolatileをつけなきゃいけないんだね。

※-Onオプションをつけて最適化しないと上記結果にはならない。

だから、シグナルハンドラや割り込みハンドラ、もしくはハードウェアがどこかのメモリ領域を書き換えてしまうような場合、変数宣言にvolatileを付けておかないと正しく読み取れなかったりするんだね。

# ちなみにC#ではlongやdoubleにはvolatileはつけられないので、longやdoubleのアトミック性の保証は別でしなければいけない。

参考:
Javaの理論と実践: volatile を扱う
DCL22-C. キャッシュできないデータには volatile を使う

0 件のコメント:

コメントを投稿