ロックなしでカウンタをインクリメント(atomic_inc_*関数)
memcachedのソース見てたら気になる処理あったのでメモ。
以下は、memcached(見たバージョンは1.4.15)の中のthread.cの関数。
unsigned short refcount_incr(unsigned short *refcount) { #ifdef HAVE_GCC_ATOMICS return __sync_add_and_fetch(refcount, 1); #elif defined(__sun) return atomic_inc_ushort_nv(refcount); #else unsigned short res; mutex_lock(&atomics_mutex); (*refcount)++; res = *refcount; mutex_unlock(&atomics_mutex); return res; #endif
やってる処理はカウントアップなんだけど、ロック機構を使用せずにカウントアップしてる処理がある。
今まで作ってたプログラムでは、マルチスレッドでのカウントアップは、ロック取ってカウントアップするような実装してたけど、こんな機構あるならこっちの方が便利そう。(環境依存にはなるけども)
開発で今使用してるOSはSolarisなので、Solarisのatomic_inc_ushort_nvのman見てみたら、
カウントアップする数値型によってそれぞれ別関数が提供されてるっぽい。
他にも、atomicな減算(atomic_dec)、計算(atomic_add)、ビット演算(atomic_bits, atomic_and, atomic_or)、スワップ処理(atomic_swap)などなど、色んな処理が用意されてるっぽい。
unsigned int型のカウントを行うatomic_inc_uint_nvを使って、サンプル作ってみる。
動作環境は、VirtualBox上のSolaris10
コンパイラは、Solaris Studio 12.3
ソース
#include <stdio.h> #include <string.h> #include <pthread.h> #include <assert.h> #include <sys/time.h> #include <atomic.h> volatile unsigned int counter = 0; #define THREAD_NUM 1000 #define COUNTUP_PER_THREAD 100000 void countup_thr_atomic() { int i; for(i=0; i<COUNTUP_PER_THREAD; i++) atomic_inc_uint_nv(&counter); } pthread_mutex_t mutexlock; void countup_thr_mutex() { int i; for(i=0; i<COUNTUP_PER_THREAD; i++) { pthread_mutex_lock(&mutexlock); counter++; pthread_mutex_unlock(&mutexlock); } } int main(int argc, char* argv[]) { void* (*thr_func)(void*); if ( argc > 1 && strcmp(argv[1], "mutex") == 0 ) { pthread_mutex_init(&mutexlock, NULL); thr_func = (void*(*)(void*))countup_thr_mutex; puts("set mutexlock countup"); } else { thr_func = (void*(*)(void*))countup_thr_atomic; puts("set atomic countup"); } struct timeval start, end; gettimeofday(&start, NULL); pthread_t tid[THREAD_NUM]; int i; for(i=0; i<THREAD_NUM; i++) pthread_create(&tid[i], NULL, thr_func, NULL); for(i=0; i<THREAD_NUM; i++) pthread_join(tid[i], NULL); gettimeofday(&end, NULL); assert(counter == THREAD_NUM * COUNTUP_PER_THREAD); printf("elapsed time:%d.%06d\n", end.tv_sec - start.tv_sec - (start.tv_usec<end.tv_usec ? 0 : 1), end.tv_usec - start.tv_usec + (start.tv_usec<end.tv_usec ? 0 : 1000000) ); return 0; }
コンパイル
性能見たいので最適化オプション付きでコンパイル。
bash-3.2$ /opt/solarisstudio12.3/bin/cc countup.c -lpthread -xO5 -o countup_thr
実行結果
atomicカウントアップ版。
bash-3.2$ ./countup_thr set atomic countup elapsed time:0.758742
mutex lockカウントアップ版。
bash-3.2$ ./countup_thr mutex set mutexlock countup elapsed time:3.507950
ということで、atomic処理の方がやっぱり早い。
何回か実行したけど、カウントが狂うこともなかった。
環境依存なソースにはなるけど、クリティカルな性能が求められる所だと使えそう。