スレッド固有データ

多くのアプリケーションでは、一定のデータを、 関数の呼び出し間でスレッドごとに維持する必要があります。

例えば、各ファイルごとに 1 つのスレッドを使用するマルチスレッドの grep コマンドは、スレッド固有のファイル・ハンドラーと検索文字列のリストを持っている必要があります。これらのニーズを満たすために、 スレッド・ライブラリーがスレッド固有のデータ・インターフェースを提供しています。

スレッド固有のデータは、キーを行索引とし、スレッド ID を列索引とする、 値の二次元配列の形で表示することができます。 スレッド固有データのキー は、隠しオブジェクトであり、データ型は pthread_key_t です。 プロセス内のすべてのスレッドで、同じキーを使用することができます。 すべてのスレッドが同じキーを使用しますが、 スレッドでは、そのキーに関連する各種のスレッド固有データ値を設定してアクセスします。 スレッド固有データはボイド・ポインターであり、これにより、動的に割り当てられた文字列や構造体などの、任意の種類のデータを参照することができます。

次の図では、スレッド T2 には、キー K3 と関連する、スレッド固有のデータ値の 12 があります。 スレッド T4 には、同じキーに関連した値 2 があります。
キー T1 スレッド T2 スレッド T3 スレッド T4 スレッド
K1 6 56 4 1
K2 87 21 0 9
K3 23 12 61 2
K4 11 76 47 88

キーの作成と破棄

スレッド固有のデータ・キーは、使用する前に作成しておく必要があります。 その値は、対応するスレッドが終了すると自動的に破棄されます。 キーも、そのストレージを再利用する要求が出されたときに破棄されます。

キーの作成

スレッド固有のデータ・キーは、 pthread_key_create サブルーチンを呼び出すことによって作成されます。 このサブルーチンは、キーを戻します。 スレッド固有データは、まだ作成されていないスレッドも含めて、 すべてのスレッドについて NULL の値に設定されます。

例えば、2 つのスレッド、AB があるとします。スレッド A は、 次の操作を順番に実行します。

  1. スレッド固有のデータ・キー K を作成します。

    スレッド AB はキー K を使用することができます。両方のスレッドの値は NULL です。

  2. スレッド C を作成します。

    スレッド C は、キー K も使用することができます。 スレッド C の値は、NULL です。

スレッド固有データ・キーの数は、1 プロセスあたり 450 までに制限されています。 この数は、PTHREAD_KEYS_MAX シンボリック定数によって検索できます。

pthread_key_create サブルーチンを呼び出すのは、 1 回だけにしなければなりません。 そうでない場合、2 つの異なるキーが作成されます。 例えば、次のコード・フラグメントの場合を考えてみます。

/* a global variable */
static pthread_key_t theKey;
 
/* thread A */
...
pthread_key_create(&theKey, NULL);   /* call 1 */
...
 
/* thread B */
...
pthread_key_create(&theKey, NULL);   /* call 2 */
...

この例では、スレッド AB は並行して実行されていますが、call 1 は call 2 より前に出されます。call 1 はキー K1 を作成し、theKey 変数に保管します。call 2 は別のキー K2 を作成し、 それも theKey 変数に保管するので、K1 が変更されます。 その結果、スレッド AK2K1 と見なして使用することになります。 このような状況は、次の理由から避けるべきです。

  • キー K1 が失われ、そのストレージはプロセスが終了するまで再利用できなくなります。 キーの数は制限されているので、十分なキーがない可能性があります。
  • スレッド A が、call 2 の前に theKey 変数を使用してスレッド固有データを保管する場合、そのデータはキー K1 に結合されます。 call 2 の後は、theKey 変数には K2 が入っているので、 スレッド A が後でそのスレッド固有データをフェッチしようとすると、 常に NULL を取得することになります。

作成されるキーの固有性を確保する方法には、次のものがあります。

  • ワンタイム初期化機能を使用する。
  • それを使用するスレッドの前にキーを作成する。 これは、例えば、類似した操作を実行するためにスレッド固有データでスレッドのプールを使用する場合、 しばしば可能です。 このスレッドのプールは、通常 1 つのスレッド、 すなわち初期 (または別の「ドライバー」) スレッドによって作成されます。

キー作成の固有性を確保するのは、プログラマーの責任です。 スレッド・ライブラリーには、キーが複数作成された場合のチェックの方法はありません。

デストラクター・ルーチン

デストラクターは、それぞれのスレッド固有のデータ・キーと関連付けることができます。 スレッドが終了したときは、NULL 以外の値 (任意のキーに結合されたこのスレッドのスレッド固有データ) がある場合、そのキーと関連したデストラクター・ルーチンが呼び出されます。 これにより、スレッドが終了したときに、 動的に割り当てられたスレッド固有データを自動的に解放することができます。 デストラクター・ルーチンには、1 つのパラメーター (スレッド固有データの値) があります。

例えば、スレッド固有データ・キーを動的に割り当てられたバッファーに使用することができます。 スレッドが終了したときにバッファーが確実に解放されるようにするためには、 デストラクター・ルーチンを準備する必要があります。 次のような free サブルーチンが使用することができます。

pthread_key_create(&key, free);
さらに複雑なデストラクターを使用することができます。 ファイルごとのスレッドを使用してスキャンを行うマルチスレッドの grep コマンドに、作業バッファーとスレッドのファイル・ディスクリプターの入った構造体を保管するためのスレッド固有データがある場合、デストラクター・ルーチンは次のようになります。
typedef struct {
        FILE *stream;
        char *buffer;
} data_t;
...

void destructor(void *data)
{
        fclose(((data_t *)data)->stream);
        free(((data_t *)data)->buffer);
        free(data);
        *data = NULL;
}

デストラクターの呼び出しは、4 回まで繰り返すことができます。

キーの破棄

スレッド固有データ・キーは、 pthread_key_delete サブルーチンを呼び出すことによって破棄できます。 pthread_key_delete サブルーチンは、 それぞれのデータを持つスレッドのために実際にデストラクター・ルーチンを呼び出すわけではありません。 データ・キーを破棄すると、 pthread_key_create サブルーチンに対する別の呼び出しで再使用できるようになります。 そのため、 pthread_key_delete サブルーチンは多くのデータ・キーを使用するときに特に有用です。 例えば、次のコード・フラグメントでは、ループが終了することはありません。

/* bad example - do not write such code! */
pthread_key_t key;
 
while (pthread_key_create(&key, NULL))
        pthread_key_delete(key);

スレッド固有データの使用

スレッド固有データは、 pthread_getspecific サブルーチンと pthread_setspecific サブルーチンを使用してアクセスします。 pthread_getspecific サブルーチンは、 指定されたキーにバインドされており、 コール側のスレッドに固有である値を読み取ります。 pthread_setspecific サブルーチンはその値を設定します。

連続する値の設定

特定のキーにバインドされる値は、任意の種類のデータを指すことができるポインターでなければなりません。 スレッド固有データは、一般に、次のコード・フラグメントのように、 動的に割り当てられたストレージに使用されます。

private_data = malloc(...);
pthread_setspecific(key, private_data);
値を設定すると、その前の値は失われます。 例えば、次のコード・フラグメントでは、 old ポインターの値が失われ、そのポインターが指していたストレージはリカバリーできない場合があります。

pthread_setspecific(key, old);
...
pthread_setspecific(key, new);
新しい値を設定する前に、 ストレージを再利用できるように古いスレッド固有のデータ値を検索するのは、 プログラマーの責任です。 例えば、次の方法で swap_specific ルーチンをインプリメントすることができます。
int swap_specific(pthread_key_t key, void **old_pt, void *new)
{
        *old_pt = pthread_getspecific(key);
        if (*old_pt == NULL)
                return -1;
        else
                return pthread_setspecific(key, new);
}

このようなルーチンは、常にスレッド固有データの以前の値を検索する必要があるわけではないので、 スレッド・ライブラリー内には存在していません。 このようなケースは、 例えば、スレッド固有データが、 初期スレッドによって割り当てられたメモリー・プール内の特定の位置を指すポインターである場合に起こります。

デストラクター・ルーチンの使用

動的に割り当てられたスレッド固有データを使用するとき、プログラマーは、 pthread_key_create サブルーチンを呼び出すときにデストラクター・ルーチンを準備する必要があります。 また、プログラマーは、スレッド固有データ用に割り当てられたストレージを解放するときに、ポインターが NULL に設定されていることを確認する必要もあります。 そうしなければ、デストラクター・ルーチンが正しくないパラメーターで呼び出される場合があります。 次に例を示します。

pthread_key_create(&key, free);
...

...
private_data = malloc(...);
pthread_setspecific(key, private_data);
...

/* bad example! */
...
pthread_getspecific(key, &data);
free(data);
...

スレッドが終了すると、 デストラクター・ルーチンがそのスレッド固有データのために呼び出されます。 その値は既に解放済みのメモリーを指すポインターなので、 エラーが起きる場合があります。 これを訂正するには、次のコード・フラグメントに置き換える必要があります。

/* better example! */
...
pthread_getspecific(key, &data);
free(data);
pthread_setspecific(key, NULL);
...

この場合、スレッドが終了したときにスレッド固有データがないので、 デストラクター・ルーチンは呼び出されません。

ポインター以外の値の使用

ポインターでない値を保管することは可能ですが、次の理由でお勧めしません。

  • ポインターをスカラー型にキャストすると、移植可能でなくなる可能性がある。
  • NULL ポインターの値はインプリメンテーションに依存しており、複数のシステムでは NULL ポインターにゼロ以外の値を割り当てています。

プログラムが別のシステムに移植されないことが確かである場合は、 スレッド固有データに整数値を使用しても構いません。