スレッド固有データ
多くのアプリケーションでは、一定のデータを、 関数の呼び出し間でスレッドごとに維持する必要があります。
例えば、各ファイルごとに 1 つのスレッドを使用するマルチスレッドの grep コマンドは、スレッド固有のファイル・ハンドラーと検索文字列のリストを持っている必要があります。これらのニーズを満たすために、 スレッド・ライブラリーがスレッド固有のデータ・インターフェースを提供しています。
スレッド固有のデータは、キーを行索引とし、スレッド ID を列索引とする、 値の二次元配列の形で表示することができます。 スレッド固有データのキー は、隠しオブジェクトであり、データ型は pthread_key_t です。 プロセス内のすべてのスレッドで、同じキーを使用することができます。 すべてのスレッドが同じキーを使用しますが、 スレッドでは、そのキーに関連する各種のスレッド固有データ値を設定してアクセスします。 スレッド固有データはボイド・ポインターであり、これにより、動的に割り当てられた文字列や構造体などの、任意の種類のデータを参照することができます。
| キー | 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 つのスレッド、A と B があるとします。スレッド A は、 次の操作を順番に実行します。
- スレッド固有のデータ・キー K を作成します。
スレッド A と B はキー K を使用することができます。両方のスレッドの値は NULL です。
- スレッド 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 */
...この例では、スレッド A と B は並行して実行されていますが、call 1 は call 2 より前に出されます。call 1 はキー K1 を作成し、theKey 変数に保管します。call 2 は別のキー K2 を作成し、 それも theKey 変数に保管するので、K1 が変更されます。 その結果、スレッド A は K2 を K1 と見なして使用することになります。 このような状況は、次の理由から避けるべきです。
- キー K1 が失われ、そのストレージはプロセスが終了するまで再利用できなくなります。 キーの数は制限されているので、十分なキーがない可能性があります。
- スレッド A が、call 2 の前に theKey 変数を使用してスレッド固有データを保管する場合、そのデータはキー K1 に結合されます。 call 2 の後は、theKey 変数には K2 が入っているので、 スレッド A が後でそのスレッド固有データをフェッチしようとすると、 常に NULL を取得することになります。
作成されるキーの固有性を確保する方法には、次のものがあります。
- ワンタイム初期化機能を使用する。
- それを使用するスレッドの前にキーを作成する。 これは、例えば、類似した操作を実行するためにスレッド固有データでスレッドのプールを使用する場合、 しばしば可能です。 このスレッドのプールは、通常 1 つのスレッド、 すなわち初期 (または別の「ドライバー」) スレッドによって作成されます。
キー作成の固有性を確保するのは、プログラマーの責任です。 スレッド・ライブラリーには、キーが複数作成された場合のチェックの方法はありません。
デストラクター・ルーチン
デストラクターは、それぞれのスレッド固有のデータ・キーと関連付けることができます。 スレッドが終了したときは、NULL 以外の値 (任意のキーに結合されたこのスレッドのスレッド固有データ) がある場合、そのキーと関連したデストラクター・ルーチンが呼び出されます。 これにより、スレッドが終了したときに、 動的に割り当てられたスレッド固有データを自動的に解放することができます。 デストラクター・ルーチンには、1 つのパラメーター (スレッド固有データの値) があります。
pthread_key_create(&key, free);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 回まで繰り返すことができます。
キーの破棄
/* 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);
pthread_setspecific(key, old);
...
pthread_setspecific(key, new);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(&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 ポインターにゼロ以外の値を割り当てています。
プログラムが別のシステムに移植されないことが確かである場合は、 スレッド固有データに整数値を使用しても構いません。