マルチスレッドのvolatile

ありがちダメなコードのダメな理由を書くコーナー。

現象。

C99+POSIX(SUSv3)のソースコード(tmp.c)

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

volatile int count = 0;
int const COUNT_PER_THREAD = 0x100000;
int const THREAD_NUM = 0x10;
void* work(void* arg) {
  for (int i = 0; i < COUNT_PER_THREAD; ++i) {
    ++count;
  }
  return arg;
}

int main() {
  pthread_t threads[THREAD_NUM];

  for (int i = 0; i < THREAD_NUM; ++i) {
    if (pthread_create(&threads[i], NULL, &work, NULL)) {
      abort();
    }
  }
  
  for (int i = 0; i < THREAD_NUM; ++i) {
    if (pthread_join(threads[i], NULL)) {
      abort();
    }
  }

  printf("total count = 0x%x, expected 0x%x*0x%x\n",
         count, THREAD_NUM, COUNT_PER_THREAD);
  
  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -pthread -Wall -Wextra -g tmp.c
$ ./a.out 
total count = 0x86b7e9, expected 0x10*0x100000
$ gcc -std=c99 -pedantic -O6 -pthread -Wall -Wextra -g tmp.c
total count = 0x87dbcb, expected 0x10*0x100000

というように、私の環境ではtotal countが予測値の半分程度になった。このtotal countの出力は実行するたびに変わり、環境によってはまったく違う値にもなるので、total count != expectedという点だけに注目してほしい。

説明。

もっとも重要なのは「C言語にはスレッドに関する規程がない」という点だ。スレッドの動作に関してはC言語の範疇ではない以上、volatile修飾すればスレッド間で共有している変数を保護できる、といったような(間違った)考えを言語仕様から導くことはできない。
その代わりに確認するのはSUSv3の記述*1

Applications shall ensure that access to any memory location by more than one thread of control (threads or processes) is restricted such that no thread of control can read or modify a memory location while another thread of control may be modifying it. Such access is restricted using functions that synchronize thread execution and also synchronize memory with respect to other threads. The following functions synchronize memory with respect to other threads:
(以下、関数リストは省略)

とある。大意は「他のスレッドが書き込んでいるかもしれないメモリは、どのスレッドも読み書きしないようにアプリケーションを書け。そういったアクセスが必要ならスレッドの実行とメモリを同期させろ。次にメモリを同期させる関数をリストアップした。」といったところ。
スレッドの実行を同期させるための関数は一通り、メモリを同期させる関数のリストに含まれている*2。逆に言えばこのリストに含まれる関数を利用しない限り、メモリの値は任意のタイミングで他のスレッドの値と同期する可能性がある。
実用上、保護されていない競合するメモリは不定値だとみなすのが適切だろう。

〆。

ちなみに同期を取っていない場合にも total count < expected は成り立ちそうに思えるかもしれないが、環境によっては total count > expected となる可能性もある。どちらにせよ、不定値には違いない。

*1:http://www.opengroup.org/onlinepubs/009695399/ 4.10 Memory Synchronization

*2:そうでなければ役に立たないが。