mokky14's IT diary

IT関係の仕事メモ、勉強会の感想など書いてます。

ロックなしでカウンタをインクリメント(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処理の方がやっぱり早い。
何回か実行したけど、カウントが狂うこともなかった。

環境依存なソースにはなるけど、クリティカルな性能が求められる所だと使えそう。