volatileとatomicの違い
volatileとatomicの違いを調べるために、以下のC++プログラムをコンパイルしてみる。
#include <atomic> void func1(int *p) { ++*p; ++*p; } void func2(volatile int *p) { ++*p; ++*p; } void func3(std::atomic_int *p) { ++*p; ++*p; }
$ g++ -std=c++11 -pthread -O2 -Wall -Wextra -g -c func.cpp -o func.o
環境による可能性はあるが、出力された機械語は端的に言うと次のようなものになる。(なおアーキテクチャはLinux x86-64)
func1: addl $2, (%rdi) ret func2: movl (%rdi), %eax addl $1, %eax movl %eax, (%rdi) movl (%rdi), %eax addl $1, %eax movl %eax, (%rdi) ret func3: lock addl $1, (%rdi) lock addl $1, (%rdi) ret
func1
は、「2を足す」という動作をしている。func2
は、「メモリから読み込んで1を足して書き込む」という動作を2回している。func3
は、「lock
しながら1を足す」という動作を2回している。
これは以下の違いによる:
volatile
は、メモリの読み込みや書き込みを、副作用を伴う動作と見なす。*1 そのため、読み込みや書き込み動作を減らす最適化を行わない。ただし、回数や順番さえ合っていればよいので、他のスレッドに干渉されるかどうかは考えない。atomic
は、他のスレッドが同時に読み書きしようとしても、あるひとまとまりの動作の間は独占的に動作するような振舞いになる。x86の場合はlock
プレフィックスで実現できる。
動作確認
この動作は以下のプログラムで確認できる。
#include <cstdio> #include <atomic> #include <thread> void func1(int *p); void func2(volatile int *p); void func3(std::atomic_int *p); void count1(int *p) { for(int i = 0; i < 1000000; ++i) { func1(p); } } void count2(volatile int *p) { for(int i = 0; i < 1000000; ++i) { func2(p); } } void count3(std::atomic_int *p) { for(int i = 0; i < 1000000; ++i) { func3(p); } } int main(int argc, char *argv[]) { int num = -1; if(argc > 1) { std::sscanf(argv[1], "%d", &num); } if(num == 1) { int x = 0; std::thread th0(&count1, &x); std::thread th1(&count1, &x); th0.join(); th1.join(); std::printf("%d\n", x); } else if(num == 2) { volatile int x = 0; std::thread th0(&count2, &x); std::thread th1(&count2, &x); th0.join(); th1.join(); std::printf("%d\n", x); } else if(num == 3) { std::atomic_int x = ATOMIC_VAR_INIT(0); std::thread th0(&count3, &x); std::thread th1(&count3, &x); th0.join(); th1.join(); std::printf("%d\n", (int)x); } return 0; }
$ g++ -std=c++11 -pthread -O2 -Wall -Wextra -g -c main.cpp -o main.o $ g++ -std=c++11 -pthread -O2 -Wall -Wextra -g func.o main.o -o main
$ ./main 1 2497250 $ ./main 1 2432386 $ ./main 1 3136510 $ ./main 1 2411326 $ ./main 1 3466956 $ ./main 1 2367656 $ ./main 1 2297168
$ ./main 2 2324260 $ ./main 2 3137164 $ ./main 2 2374254 $ ./main 2 2627152 $ ./main 2 2593840 $ ./main 2 2871581 $ ./main 2 2218822 $ ./main 2 2617198
$ ./main 3 4000000 $ ./main 3 4000000 $ ./main 3 4000000 $ ./main 3 4000000 $ ./main 3 4000000 $ ./main 3 4000000 $ ./main 3 4000000
これはマルチコアの動作結果に関わるので環境によって異なる動作をするかもしれない。手元の環境はVirtualBoxで仮想化されたLinux x86-64であった。
atomicでは不十分な場合もある
atomicで一まとまりの動作と見なされる範囲はごく小さい。例えば*p
が偶数であったとすると、以下の関数を繰り返し実行しても*p
の値が変わらないことが意図される(シングルスレッドではそうなる)が、マルチスレッドではそうはならない。
#include <atomic> void func(std:atomic_int *p) { *p += 1; *p ^= 1; }
このような場合は単に*p
をatomicにするだけでは不十分だが、mutexなどを使えばうまくいく。
*1:これはマルチスレッドのための仕組みというよりも、Memory-Mapped I/Oなどでメモリの読み書きが動作を伴う場合が想定されていると思われる。