2006年8月10日

再入不可能な関数を C で実装する

一度実行したら二度と中身を実行できなくなる再入不可能な関数を C で実装してみます。通常、このような関数はシングルトンなどの静的なデータの初期化に使いますが、ここではデータについては考えないことにします。

 

static 変数をフラグに使う

まずは最も単純な方法から見ていきます。次の関数は static 変数をフラグに使って再入を防いでいます。厳密に言えば関数そのものには入ってしまっていますが、ここで気にしないことにします。

void once(void) {
  static int entered;  // 最初は 0
  if (entered == 1) {  // すでに入ったことがある場合は
    return;  // すぐ出る
  }
  entered = 1;
  // 初回の場合のみ、何かを実行する
}

この方法はシングルスレッドのプログラムではうまく動きますが、マルチスレッドの場合は if 文で比較をする部分と entered に 1 を代入する部分の間にレースコンディションが存在するため、破綻します。

pthread の mutex を使う

次の方法ではレースコンディションを防ぐために、比較と代入の部分を pthread の mutex で排他制御しています。これでマルチスレッドでも大丈夫になりました。

#include <pthread.h>

static pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;

void once(void) {
  static int entered;  // 最初は 0
  // 比較と代入の部分を Mutex で排他制御する
  pthread_mutex_lock(&mutex);
  int tmp = (entered == 1);
  entered = 1;
  pthread_mutex_unlock(&mutex);

  if (tmp) {  // すでに入ったことがある場合は
    return;  // すぐ出る
  }
  // 初回の場合のみ、何かを実行する
}

pthread_onceを使う

また、pthread にはそのものずばり pthread_once() という、関数を一度だけ実行するための関数があります。こちらの方が簡単です。

#include <pthread.h>

static void once_internal(void) {
  // この関数は一度しか実行されない
}

static pthread_once_t entered = PTHREAD_ONCE_INIT;

void once(void)
{
  pthread_once(&entered, once_internal);
}

アトミック命令を使う

マルチスレッドの問題は解決しましたが、非同期シグナルハンドラから同時多発的に呼ばれた場合を考えると、pthread の関数は使えません。そこで、 x86 の 486 以降に備わっている cmpxchg 命令を使って排他制御を行ってみます。

void once(void) {
  // int は atomic な読み書きが可能
  volatile static int entered; // 最初は 0
  int result;

  // 次のような操作を行う
  // - lock でバスをロックする[1]
  // - メモリ内の entered とレジスタ EAX 内の 0を較べて
  //   - 同じだったら任意のレジスタ[2]内の 1 を entered に入れる (初回)
  //   - 違ったら entered の中身を EAX に入れる[3] (2度目以降)
  // - EAX の内容が result に入る[4]
  //
  // [1] マルチプロセッサで安全に動作させるため
  // [2] 任意のレジスタ」なのは "r" を指定しているため
  // [3] 2度目以降は entered はすでに 1 になっている
  // [4] result の値は初回は 0 で、2度目以降は1になる
  __asm__ __volatile__("lock; cmpxchg %1, %2\n"
                       : "=a" (result)
                       : "r" (1), "m" (entered), "0" (0)
                       : "memory");

  if (result == 1) {  // すでに入ったことがある場合は
    return;  // すぐ出る
  }
  // 初回の場合のみ、何かを実行する
}

この方法を使うと、静的変数である entered に対する比較と代入がアトミックに行われます。レジスタの内容やスタック上のローカル変数は、他のスレッドや他のシグナルのコンテキストとは共有されていないため問題ありません。

まとめ

一度実行したら二度と中身を実行できなくなる再入不可能な関数を C で実装する方法を紹介しました。このような手法が役立つことは滅多にないと思いますが、シグナル関連の厄介なバグを退治するときなどにもしかしたら使えるかもしれません。

参考サイト

すべて memologueさんから。このサイトには他にも Unix のシステムプログラミングに関するおもしろい内容が満載です。かなりお勧めです。