再入可能コードおよびスレッド・セーフ・コードの作成
単一スレッドのプロセスには、制御の流れが 1 つしかありません。 したがって、これらのプロセスによって実行されるコードは、再入可能またはスレッド・セーフのコードである必要はありません。 マルチスレッド・プログラムでは、同じ関数や同じリソースが複数の制御の流れによって同時にアクセスされる可能性があります。
リソースの保全性を保護するために、マルチスレッド・プログラム用に作成されたコードは、再入可能かつスレッド・セーフのコードである必要があります。
再入可能およびスレッド・セーフティーは、 どちらも関数によるリソースの操作方法に関係しています。 再入可能とスレッド・セーフティーは別の概念です。関数は、再入可能であるか、スレッド・セーフであるか、その両方であるか、またはどちらでもないか、のいずれかです。
この項では、再入可能およびスレッド・セーフのプログラム作成に関する情報を提供します。 効率的なスレッドのプログラム作成に関するトピックは取り上げていません。 効率的なスレッドのプログラムは、効率よく並行化されたプログラムです。 プログラムの設計時には、スレッドの効率を考慮する必要があります。 既存の単一スレッド・プログラムでスレッドを効率化することは可能ですが、 そのためには完全に設計し直して書き直す必要があります。
再入
再入可能関数は、連続する呼び出しで静的データを保持せず、 静的データを指すポインターを戻すこともありません。 すべてのデータは関数の呼び出し元によって提供されます。 再入可能関数は、再入不可関数を呼び出してはなりません。
再入不可関数は、しばしば (常にではありませんが)、 その外部インターフェースと使用法とによって識別することができます。 例えば、 strtok サブルーチンは、トークンに分割されるストリングを保持するため、再入可能ではありません。 ctime サブルーチンも再入可能ではありません。呼び出しごとに上書きされる静的データへのポインターを戻します。
スレッド・セーフティー
スレッド・セーフ関数は、ロックを使用して共有リソースを同時アクセスから保護します。 スレッド・セーフティーは、関数のインプリメンテーションだけに関係しており、 その外部インターフェースには影響しません。
/* threadsafe function */
int diff(int x, int y)
{
int delta;
delta = y - x;
if (delta < 0)
delta = -delta;
return delta;
}グローバル・データの使用は、スレッド・アンセーフです。 グローバル・データは、スレッドごとに保守するか、 またはカプセル化して、アクセスを逐次化できるようにする必要があります。 スレッドは、他のスレッドによるエラーに対応するエラー・コードを読んでしまうことがあります。 AIX®では、各スレッドが独自のerrno値を持つ。
関数の再入可能化
ほとんどの場合、再入不可関数は、 インターフェースを再入可能に変更した関数に置き換える必要があります。 再入不可関数は、複数のスレッドでは使用できません。 さらに、再入不可関数はスレッド・セーフにできない場合もあります。
戻りデータ
- 動的に割り当てられたデータを戻す。 この場合、ストレージの解放はコール側の責任になります。 この方法の利点は、インターフェースを変更する必要がないということです。 ただし、逆方向の互換性は確保されていません。 変更された関数を使用する既存の単一スレッドのプログラムを変更しないと、 ストレージが解放されないので、メモリー・リークの原因となります。
- コール側が提供したストレージを使用する。 インターフェースの変更が必要ですが、この方法をお勧めします。
/* non-reentrant function */
char *strtoupper(char *string)
{
static char buffer[MAX_STRING_SIZE];
int index;
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}/* reentrant function (a poor solution) */
char *strtoupper(char *string)
{
char *buffer;
int index;
/* error-checking should be performed! */
buffer = malloc(MAX_STRING_SIZE);
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}/* reentrant function (a better solution) */
char *strtoupper_r(char *in_str, char *out_str)
{
int index;
for (index = 0; in_str[index]; index++)
out_str[index] = toupper(in_str[index]);
out_str[index] = 0
return out_str;
}再入不可の標準 C ライブラリー・サブルーチンは、コール側が提供するストレージを使用して再入可能になっています。
連続呼び出しでのデータの保持
連続して呼び出す場合には、データを保持すべきではありません。 異なるスレッドが、連続してその関数を呼び出す可能性があるからです。 関数が連続して呼び出されるときに、 作業バッファーやポインターなどの一部のデータを保持しておく必要がある場合は、コール側がこのデータを提供するようにしてください。
/* non-reentrant function */
char lowercase_c(char *string)
{
static char *buffer;
static int index;
char c = 0;
/* stores the string on first call */
if (string != NULL) {
buffer = string;
index = 0;
}
/* searches a lowercase character */
for (; c = buffer[index]; index++) {
if (islower(c)) {
index++;
break;
}
}
return c;
}h
/* reentrant function */
char reentrant_lowercase_c(char *string, int *p_index)
{
char c = 0;
/* no initialization - the caller should have done it */
/* searches a lowercase character */
for (; c = string[*p_index]; (*p_index)++) {
if (islower(c)) {
(*p_index)++;
break;
}
}
return c;
}char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
...
}関数をスレッド・セーフにする
マルチスレッド・プログラムでは、複数のスレッドによって呼び出されるすべての関数がスレッド・セーフでなければなりません。 しかし、マルチスレッド・プログラムでスレッド・セーフでないサブルーチンを使用するための予備手段があります。 再入不可関数は通常、スレッド・アンセーフですが、これを再入可能にすると、スレッド・セーフにもなります。
共用リソースのロック
/* thread-unsafe function */
int increment_counter()
{
static int counter = 0;
counter++;
return counter;
}/* pseudo-code threadsafe function */
int increment_counter();
{
static int counter = 0;
static lock_type counter_lock = LOCK_INITIALIZER;
pthread_mutex_lock(counter_lock);
counter++;
pthread_mutex_unlock(counter_lock);
return counter;
}スレッド・ライブラリーを使用するマルチスレッド・アプリケーション・プログラムでは、共有リソースの逐次化のために mutex を使用する必要があります。 独立ライブラリーは、スレッドのコンテキストの外側で働く必要がある場合があるので、 他の種類のロックを使用します。
スレッド・アンセーフ関数の回避策
- ライブラリーにグローバル・ロックを使用して、
ライブラリーを使用する
(ライブラリー・ルーチンを呼び出したり、ライブラリー・グローバル変数を使用する) ごとにロックします。 この解決策では、
任意の時点でライブラリーの任意の部分にアクセスできるのは
1 つのスレッドだけなので、パフォーマンスのボトルネックが生じる可能性があります。 次の疑似コードの解決策を許容できるのは、ライブラリーがほとんどアクセスされないか、または初期の迅速に
インプリメントするための手段とする場合だけです。
/* this is pseudo code! */ lock(library_lock); library_call(); unlock(library_lock); lock(library_lock); x = library_var; unlock(library_lock); - ライブラリー・コンポーネント (ルーチンまたはグローバル変数)
またはコンポーネントのグループごとに、ロックを使用します。 この解決策は、上記の例よりもインプリメントする方法が多少複雑ですが、
パフォーマンスを改善することができます。 この方法は、アプリケーション・プログラムでのみ使用します。
ライブラリーで使用してはなりませんが、mutex をライブラリーのロックに使用することができます。
/* this is pseudo-code! */ lock(library_moduleA_lock); library_moduleA_call(); unlock(library_moduleA_lock); lock(library_moduleB_lock); x = library_moduleB_var; unlock(library_moduleB_lock);
再入可能およびスレッド・セーフ・ライブラリー
再入可能かつスレッド・セーフのライブラリーは、スレッド内部だけでなく、幅広い範囲の並列 (および非同期) プログラミング環境で有用です。 したがって、プログラミングの際は、常に再入可能かつスレッド・セーフの関数を使用し作成することをお勧めします。
ライブラリーの使用
- 標準 C ライブラリー (libc.a)
- バークレー互換ライブラリー (libbsd.a)
標準 C サブルーチンの一部 (ctime や strtok など) は、 再入不可です。 再入可能バージョンのサブルーチンの名前は、 オリジナルのサブルーチンの名前に接尾部 _r (下線 + r) が付いています。
token[0] = strtok(string, separators);
i = 0;
do {
i++;
token[i] = strtok(NULL, separators);
} while (token[i] != NULL);char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
i++;
token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);スレッド・アンセーフ・ライブラリーは、 プログラムで 1 つのスレッドでしか使用できません。 ライブラリーを使用するスレッドが 1 つだけであることを確認してください。 そうでなければ、プログラムは想定外の振る舞いをしたり、停止する場合さえあります。
ライブラリーの変換
- エクスポートされたグローバル変数を識別する。 これらの変数は、 通常、ヘッダー・ファイルで export キーワードによって定義されます。 エクスポートされたグローバル変数は、カプセル化する必要があります。 この変数はプライベートにする必要があり (ライブラリーのソース・コード の static キーワードで定義される)、アクセス (読み取りおよび書き込み) サブルーチンが作成されなければなりません。
- 静的変数およびその他の共有リソースを識別する。 静的変数は、普通 static キーワードによって定義されます。 どの共有リソースにもロックを関連付ける必要があります。 ロックの細分性、すなわちロックの数の選択によって、 ライブラリーのパフォーマンスが影響を受けます。 ロックを初期化するには、ワンタイム初期化機能を使用することができます。
- 再入不可関数を識別し、それらの関数を再入可能にする。 詳しくは、 関数を再入可能にするを参照してください。
- スレッド・アンセーフ関数を識別し、それらの関数をスレッド・セーフにする。 詳しくは、 関数をスレッド・セーフにするを参照してください。