IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

最初の一回だけ初期化したいとき

一日一回スレッド勉強

最初の一回だけ初期化したいとき

以下のように書く

// まったくセンスのない例ですが
#include <stdio.h>
#include <pthread.h>

// この p を
static char* p = NULL;
static pthread_mutex_t m;

void* f(void* _p) {

    // 最初にここを通ったときだけ初期化したい
    pthread_mutex_lock(&m);
    if (p == NULL) {
        char* tmp = (char*)malloc(10);
        strcpy(tmp, "hoge");
        p = tmp;
    }   
    pthread_mutex_unlock(&m);

    *(char**)_p = p;
}

int main() {
    pthread_t t0, t1; 
    char* p0; 
    char* p1; 

    pthread_create(&t0, NULL, f, &p0);
    pthread_create(&t1, NULL, f, &p1);

    pthread_join(t0, NULL);
    pthread_join(t1, NULL);

    printf("%p %p\n", p0, p1);

    return 0;
}

でも

これだと、2回目以降に毎回必要のないロックがかかることになる。
遅い

なので

// ここを volatile にする
// (この変数の値はアトミック(つまり、レジスタにだけあってメモリにないということがない変数に)になる)
volatile char* p = NULL;
pthread_mutex_t m;

void* f(void* _p) {

    // ロックかからない
    if (p == NULL) {
        pthread_mutex_lock(&m);

        // ここからはクリティカルセクション

        // 一個目の初期化時にここでブロックしたスレッドのために
        // もう一回 NULL チェック
        if (p == NULL) {

            // ここではまだ p に代入しない
            // 代入したら別スレッドで初期化されていない p が返ってしまう
            char* tmp = (char*)malloc(10);
            strcpy(tmp, "hoge");

            // クリティカルセクションの最後で代入
            p = tmp;
        }   
        pthread_mutex_unlock(&m);
    }   

    *(char**)_p = p;
}

こういうのを

Double Checked Locking Optimize というらしい

試してみた

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

#ifdef DCL
volatile char* p = NULL;
#endif

#ifndef DCL
static char* p = NULL;
#endif

pthread_mutex_t m;

void* f(void* _p) {


#ifdef DCL
    if (p == NULL) {
#endif
        pthread_mutex_lock(&m);
        if (p == NULL) {
            char* tmp = (char*)malloc(10);
            strcpy(tmp, "hoge");
            p = tmp;
        }
        pthread_mutex_unlock(&m);
#ifdef DCL
    }
#endif

    *(char**)_p = p;
}

#define NUM 0x1000
int main() {
    pthread_t t[NUM];
    char* p[NUM];

    int i;
    for (i = 0; i < NUM; i ++)
        pthread_create(t + i, NULL, f, p + i);

    for (i = 0; i < NUM; i ++) {
        pthread_join(t[i], NULL);
        if (p[0] != p[i]) puts("ERROR!!");
    }

    puts("");

    return 0;
}
$ gcc -DDCL volatile.c  && time ./a.out && gcc volatile.c && time ./a.out
volatile.c: In function ‘f’:
volatile.c:37: warning: assignment discards qualifiers from pointer target type


real	0m0.557s
user	0m0.082s
sys	0m0.569s


real	0m0.504s
user	0m0.080s
sys	0m0.536s

あれ、全然変わらないなあ。

(追記)このコードはバグッてるみたいです><

基本的にメモリバリアーを理解してないと、DCLパターンは地雷。

http://mkosaki.blog46.fc2.com/blog-entry-552.html

ありがとうございます><
次の勉強はメモリバリアーですね!

(追記) pthread_once

pthread_once という、まさにこのためのような関数があるんですね。
トラックバックや、コメントで教えてくださった皆様ありがとうございます!!
以下、スニペット

pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_once(&once, initFunc);