はじめに
マルチスレッドで構成されたプログラムで同じ変数(メモリ)にアクセスする場合、
値を変更するタイミングと参照するタイミングが同時に起きると、意図した動作とならない場合がある。
この現象を防ぐ手段として、mutex などを使ってスレッド間の処理を調停する仕組みが用意されており、
pthread_mutex_lock などの関数を使ったことのある方も多いと思います。
今回は、mutex 以外の手段として、GCC Builtin 関数をご紹介いたします。
参考
pthread 系の関数を使用する際の注意点を以下で紹介しています。ご参考まで。
意図しない動作とは?
先ずは、意図しない動作を行うサンプルプログラムを書き、動かしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#include <pthread.h> #include <stdio.h> #include <string.h> #include <unistd.h> #define MAX_INDEX (30000) #define SET_VALUE (100) unsigned int pos = 0; unsigned char array_log[MAX_INDEX]; static void check_and_exit(void) { int i, ng_count = 0; for (i = 0; i < MAX_INDEX; i++) if (array_log[i] != SET_VALUE) { printf("invalid array_log[%d] = %d\n", i, array_log[i]); ng_count++; } printf("ng_count=%d\n", ng_count); _exit(0); } static void output_log(unsigned char val) { unsigned int idx = pos++; array_log[idx] += val; if (pos >= MAX_INDEX) check_and_exit(); } static void * thread_main(void *arg) { while (1) { if (pos < MAX_INDEX) output_log(SET_VALUE); } return NULL; } int main(void) { memset(array_log, 0, sizeof(array_log)); pthread_t td; pthread_create(&td, NULL, thread_main, NULL); while(1) { if (pos < MAX_INDEX) output_log(SET_VALUE); } return 0; } |
実行結果
(略)
ng_count=720
解説
プログラムはいたってシンプル。
30000個の要素数を持つ array_log に、100 という数値を加算します。
配列の要素の位置を pos で覚えており、output_log() が呼び出されるたびにインクリメントします。
シングルスレッドで動作させれば全ての要素に 100 がセットされることになり、
最後の check_and_exit() で ng_count がインクリメントされることなく意図通りの結果となります。
しかし、今回は 2 つのスレッドから output_log() を呼び出しており、
値が 200 になっている要素が複数存在する結果となりました。
これは、2 つのスレッドから同じ要素に加算したことを意味します。
全て 100 となることが期待値に対して、意図しない結果となります。
スレッドを生成して動き始めるまでに時間を要するため、競合の発生が遅れるためです。
なぜ?
なぜこのような現象が起きるのでしょうか?
詳細は割愛しますが、1 つのスレッドが pos を読み出すタイミングと、
もう 1 つのスレッドが pos を読み出すタイミングが同時になり、同じ要素へ書き込んでしまうためです。
GCC Builtinsを使わない場合
このようなスレッドの競合を防ぐ手段として、pthread_mutex_lock をよく使用します。
mutex を定義し、初期化して、競合が発生すると都合の悪い処理を lock/unlock で挟みます。
先ほどのサンプルプログラムでは、以下の★のような変更が必要になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
(snip) pthread_mutex_t mutex; //★mutexの定義 (snip) static void output_log(unsigned char val) { pthread_mutex_lock(&mutex); //★mutex ロック unsigned int idx = pos++; pthread_mutex_unlock(&mutex); //★mutex アンロック (snip) } (snip) int main(void) { memset(array_log, 0, sizeof(array_log)); pthread_mutex_init(&mutex, NULL); //★mutex の初期化 (snip) return 0; } |
実行結果
ng_count=0
pthread ライブラリが提供している排他処理の仕組みを使うと、
意図した動作にすることができます。
しかし。。。とても面倒。
一つの変数を守るために追加しなければならない処理手順が多く、
そして mutex を宣言することでメモリも余分に消費します。
GCC Builtins を使った場合
そこで今回ご紹介する GCC Builtins を使うとどうなるのか、さっそく見ていきましょう。
1 2 3 4 5 6 |
static void output_log(unsigned char val) { unsigned int idx = __sync_fetch_and_add(&pos, 1); (snip) } |
実行結果
ng_count=0
GCC Builtins 関数の __sync 系は、Atomic Memory Access を提供しています。
要は、メモリバリア付きのアクセスを行うアセンブラを、コンパイル時に吐き出してくれます。
詳しくは公式ページで紹介されていますが、今回の「加算」だけではなく、
減算、論理演算に対する関数も提供されています。
また、加算前の値を取得するのか、加算後の値を取得するのか、
どちらに対応する場合でも、それらの関数が提供されています。
例えば、先ほどの __sync_fetch_and_add は加算前の値を取得して加算、
対する __sync_add_and_fetch は加算後の値を取得することができます。
Tips
いやいや、先ほどのログの例の場合、要素を保持する index はループするのが普通でしょ!
と、思われたかもしれません。
ループさせたい場合は、if 文で pos の値をチェックし、最大数を超えていたら 0 にする必要があります。
もちろん、この区間も含めて排他する必要があり、GCC Builtins だけでは実現できません。
上記のような処理を行いたい場合は、以下のように記述します。
1 2 3 4 5 6 7 8 9 10 11 |
#define MAX_INDEX (0x1000) //★ (snip) static void output_log(unsigned char val) { unsigned int idx = __sync_fetch_and_add(&pos, 1); idx &= (MAX_INDEX - 1); (snip) } |
要素位置を記憶している pos は永遠にインクリメントしていき、
使用する際に AND を使って正しい位置を算出します。
AND する値は 0x1000 - 1 = 0xFFF とすることで、要素数を超えた pos=0x1000 の場合は idx=0、
pos=0x1001 の場合は idx=1 という要素位置を得ることができるようになります。