IT戦記

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

spin_lock を使ってみる(再挑戦)

spin_lock は wait するようなところで使っちゃいけない

以下のエントリで
spin_lock とは - IT戦記
id:kosaki さんからコメントを頂きました!

たぶん気づいていると思いますが、waitが必要な場合は、そもそもspin_lockが適切じゃないケースなのでspin lockでキューを作るのは、あんまり例としてよろしくない気がします

色々と読んでいるうちに、僕の spin_lock の使い方が「これはひどい」ということが分かりました。
というわけで、もう一度スレッドを使ったコードを書いてみようと思います。

考えたこと

spin_lock の使いどころは、キューなどではなくもっともっと小さなロック。
なので、例えばスレッド間で共有している変数の値を変えるためだけのロックなどに使える。(pthread_once でやるようなこと)
たかだか、メモリに値を入れるだけならスレッドを休ませるより走らせっぱなしのほうが効率が良さそう。

というわけで

さっきのエントリ
pthread でキューを作ってみる(再々挑戦、最終版) - IT戦記
の初期化の箇所をわざわざ spin_lock を使って最初の一回目の enqueue/dequeue の時に初期化するように改造してみます。

構造体
// 構造体: queue_t
typedef struct queue {
    unsigned int    lock;       // ** 初期化用のロック
    unsigned int    inited;     // ** 初期化済みフラグ
    unsigned int    in;         // 次に入れるインデックス
    unsigned int    out;        // 次に出すインデックス
    unsigned int    size;       // キューのサイズ
    unsigned int    length;     // キューに格納されている要素数
    pthread_mutex_t mutex;
    pthread_cond_t  not_full;   // キューが満タンじゃないという条件(cond)
    pthread_cond_t  not_empty;  // キューが空じゃないという条件(cond)
    void*           buffer[1];  // バッファ
    // ここ以下のメモリは p_queue->buffer[3] などとして参照される
} queue_t;
メモリ確保
queue_t* create_queue (size_t size) {
    int memsize = sizeof(queue_t) + (size - 1) * sizeof(void*);
    queue_t* p_queue = (queue_t*)malloc(memsize);
    p_queue->lock = 0;
    p_queue->inited = 0;
    p_queue->size = size;
    return p_queue;
}
初期化
void init_queue(queue_t* p_queue) {

    assert(p_queue != NULL);

    // ここからスピンロック
    spin_lock(&p_queue->lock);

    if (!p_queue->inited) {
        p_queue->inited = 1;

        // index の初期化
        p_queue->in = 0;
        p_queue->out = 0;

        // 要素数の初期化
        p_queue->length = 0;

        // pthread_mutex_t の初期化
        pthread_mutex_init(&p_queue->mutex, NULL);

        // pthread_cond_t の初期化
        pthread_cond_init(&p_queue->not_full, NULL);
        pthread_cond_init(&p_queue->not_empty, NULL);
    }

    // ここまでスピンロック
    spin_unlock(&p_queue->lock);
}
こうしておけば

init_queue はスレッドセーフになる。しかも、 mutex より速い。
で、 enqueue と dequeue の最初で init_queue を呼べばいい。

あと

pthread_once の実装を見てみたら spin_lock が使われてた!

/*
 * Execute a function exactly one time in a thread-safe fashion.
 */
int     
pthread_once(pthread_once_t *once_control, 
         void (*init_routine)(void))
{
    _spin_lock(&once_control->lock);
    if (once_control->sig == _PTHREAD_ONCE_SIG_init)
    {    
        pthread_cleanup_push(__pthread_once_cancel_handler, once_control);
        (*init_routine)();
        pthread_cleanup_pop(0);
        once_control->sig = _PTHREAD_ONCE_SIG;
    }    
    _spin_unlock(&once_control->lock);
    return (0);  /* Spec defines no possible errors! */
}

まとめ

spin_lock は wait するような所では使わない。もっともっと小さなロック。