2009-12-10[木] C標準ライブラリのマルチスレッド対応のメモ

グローバル変数や関数内部のstatic変数、あるいはそれらにアクセスする関数は、 複数のスレッドから同時に読み書きが行われると破綻する(バグになる).

C標準ライブラリで(特にマルチスレッドセーフに作っていない場合)そのようなものは

  • グローバル変数(風のもの). stderr,stdin,stdout,stderr
  • rand, srand, strtok, tmpnam
  • stdin,stdoutを暗黙に使うモノ(printf,puts,putc,getc,scanf...)
  • errnoを書換えるモノ全般(ファイル関係:fopen,fgets,fputs,fread,fwrite,..等)
  • ロケール関係(setlocale, およびロケールの影響を受けるstring系関数,時間系関数等)
  • (malloc関係)

等結構ある.(malloc関係はちょっと別枠だけど)


マルチスレッド対応の方法としては、たとえば元が

static unsigned long rand_seed = 1;
int rand(void) {
    rand_seed = rand_seed * 1103515245L + 12345;
    return (rand_seed >> 16) & 0x7fff;
}

て感じだとすると, 雰囲気として

struct ThreadLocalVar {           // この構造体は適当
    long  rand_seed;              // rand,srand
    char* strtok_ptr;             // strtok
    char  tmpnam_buf[_MAX_PATH];  // tmpnam
    int   errno_wk;               // errno
       :
     (その他標準ライブラリが使う変数全部)
       :
};
ThreadLocalVar* get_threadLocalVar() {
    return 現在のスレッドのThreadLocalへのポインタを返す;
}
int rand(void) {
    ThreadLocalVar* p = get_threadLocalVar();
    p->rand_seed = p->rand_seed * 1103515245L + 12345;
    return (p->rand_seed >> 16) & 0x7fff;
}

といった感じに、ライブラリが内部で使う変数すべてを収めた構造体を用意してスレッド別にそのメモリを持つ. スレッドごとに独立して処理するため、他スレッドの影響が起きないようになる.

当然、スレッドの開始時にはこの構造体の初期化が必要になる.
vcでC標準ライブラリ使っている場合にスレッド生成するなら、
 CreateThreadEx でなく _beginthreadex
を使う必要があるのは、この手の初期化も行うため.

あと、上記では、適当にget_threadLocalVar()の名で中身はしょってるけど、 vcだと_getptd()、newlibなんかだと _REENT となっているので実際はそれらを みてみれば. (_REENTはちょっと意味違うものだから紛らわしかったかも)

ようは、スレッド別のメモリのポインタの取得は、 グローバル変数アクセスごとにmutex等で制御したりするのに比べれば まま軽いだろう(そこまで非効率な実装にはなってなかったよと).

ただ strtok のような本来十分に軽い処理からすれば (そうであることを前提に利用頻度が高い場合)、 スレッド別のメモリのポインタの取得は気になるかもで、 可能なら使わずにすます形にしていったほうがよいだろう.
(汎用性は落ちるけど、vcなら strtok_s, unix/linux系なら strtok_r, に 置き換えるのも手)


C++0xではスレッドローカル記憶域(TLS)が追加されるかもらしい.
もっともvcにしろgccにしろ他のコンパイラにしろ、結構すでに サポート済みのよう. (wikipedia)

そういやD言語ver2もサポートしてる. D2の場合は特に指定のないグローバル変数はスレッドローカルになる.