再入可能コードおよびスレッド・セーフ・コードの作成

単一スレッドのプロセスには、制御の流れが 1 つしかありません。 したがって、これらのプロセスによって実行されるコードは、再入可能またはスレッド・セーフのコードである必要はありません。 マルチスレッド・プログラムでは、同じ関数や同じリソースが複数の制御の流れによって同時にアクセスされる可能性があります。

リソースの保全性を保護するために、マルチスレッド・プログラム用に作成されたコードは、再入可能かつスレッド・セーフのコードである必要があります。

再入可能およびスレッド・セーフティーは、 どちらも関数によるリソースの操作方法に関係しています。 再入可能とスレッド・セーフティーは別の概念です。関数は、再入可能であるか、スレッド・セーフであるか、その両方であるか、またはどちらでもないか、のいずれかです。

この項では、再入可能およびスレッド・セーフのプログラム作成に関する情報を提供します。 効率的なスレッドのプログラム作成に関するトピックは取り上げていません。 効率的なスレッドのプログラムは、効率よく並行化されたプログラムです。 プログラムの設計時には、スレッドの効率を考慮する必要があります。 既存の単一スレッド・プログラムでスレッドを効率化することは可能ですが、 そのためには完全に設計し直して書き直す必要があります。

再入

再入可能関数は、連続する呼び出しで静的データを保持せず、 静的データを指すポインターを戻すこともありません。 すべてのデータは関数の呼び出し元によって提供されます。 再入可能関数は、再入不可関数を呼び出してはなりません。

再入不可関数は、しばしば (常にではありませんが)、 その外部インターフェースと使用法とによって識別することができます。 例えば、 strtok サブルーチンは、トークンに分割されるストリングを保持するため、再入可能ではありません。 ctime サブルーチンも再入可能ではありません。呼び出しごとに上書きされる静的データへのポインターを戻します。

スレッド・セーフティー

スレッド・セーフ関数は、ロックを使用して共有リソースを同時アクセスから保護します。 スレッド・セーフティーは、関数のインプリメンテーションだけに関係しており、 その外部インターフェースには影響しません。

C 言語では、ローカル変数はスタック上で動的に割り当てられます。 したがって、次の例のように、静的データやその他の共有リソースを使用しない関数は、当然スレッド・セーフです。
/* threadsafe function */
int diff(int x, int y)
{
        int delta;

        delta = y - x;
        if (delta < 0)
                delta = -delta;

        return delta;
}

グローバル・データの使用は、スレッド・アンセーフです。 グローバル・データは、スレッドごとに保守するか、 またはカプセル化して、アクセスを逐次化できるようにする必要があります。 スレッドは、他のスレッドによるエラーに対応するエラー・コードを読んでしまうことがあります。 AIX®では、各スレッドが独自のerrno値を持つ。

関数の再入可能化

ほとんどの場合、再入不可関数は、 インターフェースを再入可能に変更した関数に置き換える必要があります。 再入不可関数は、複数のスレッドでは使用できません。 さらに、再入不可関数はスレッド・セーフにできない場合もあります。

戻りデータ

多くの再入不可関数は、静的データを指すポインターを戻します。 これは、次の方法で回避することができます。
  • 動的に割り当てられたデータを戻す。 この場合、ストレージの解放はコール側の責任になります。 この方法の利点は、インターフェースを変更する必要がないということです。 ただし、逆方向の互換性は確保されていません。 変更された関数を使用する既存の単一スレッドのプログラムを変更しないと、 ストレージが解放されないので、メモリー・リークの原因となります。
  • コール側が提供したストレージを使用する。 インターフェースの変更が必要ですが、この方法をお勧めします。
例えば、strtoupper 関数は、 文字列を英大文字に変換するものですが、 次のコード・フラグメントのようにインプリメントすることができます。
/* 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 ライブラリー・サブルーチンは、コール側が提供するストレージを使用して再入可能になっています。

連続呼び出しでのデータの保持

連続して呼び出す場合には、データを保持すべきではありません。 異なるスレッドが、連続してその関数を呼び出す可能性があるからです。 関数が連続して呼び出されるときに、 作業バッファーやポインターなどの一部のデータを保持しておく必要がある場合は、コール側がこのデータを提供するようにしてください。

次の例を考えてみましょう。 関数は、連続した英小文字の文字列を戻します。 文字列は、最初の呼び出しだけで、 strtok サブルーチンの場合のように、提供されます。 関数は、文字列の終わりに達すると 0 を戻します。 この関数は、次のコード・フラグメントのようにインプリメントできます。
/* 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

この関数は、再入可能ではありません。 これを再入可能にするには、 静的データ (index 変数) をコール側が保守する必要があります。 この関数の再入可能バージョンは、 次のコード・フラグメントのようにインプリメントすることができます。
/* 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;
}
関数のインターフェースが変更され、使用法も変更されました。 呼び出し元は、次のコード・フラグメントのように、各呼び出しごとに文字列を提供し、 最初の呼び出しの前に索引を 0 に初期化する必要があります。
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;
}
スレッド・セーフにするには、次の例のように、静的ロックによって静的変数 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);

再入可能およびスレッド・セーフ・ライブラリー

再入可能かつスレッド・セーフのライブラリーは、スレッド内部だけでなく、幅広い範囲の並列 (および非同期) プログラミング環境で有用です。 したがって、プログラミングの際は、常に再入可能かつスレッド・セーフの関数を使用し作成することをお勧めします。

ライブラリーの使用

AIX 基本オペレーティング・システムに同梱されているいくつかのライブラリーは、スレッド・セーフです。 現行バージョンの AIXでは、以下のライブラリーがスレッド・セーフです。
  • 標準 C ライブラリー (libc.a)
  • バークレー互換ライブラリー (libbsd.a)

標準 C サブルーチンの一部 (ctimestrtok など) は、 再入不可です。 再入可能バージョンのサブルーチンの名前は、 オリジナルのサブルーチンの名前に接尾部 _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 つだけであることを確認してください。 そうでなければ、プログラムは想定外の振る舞いをしたり、停止する場合さえあります。

ライブラリーの変換

既存のライブラリーを再入可能かつスレッド・セーフのライブラリーに変換する場合は、次の項目を考慮してください。 この情報は、C 言語ライブラリーだけに適用されます。
  • エクスポートされたグローバル変数を識別する。 これらの変数は、 通常、ヘッダー・ファイルで export キーワードによって定義されます。 エクスポートされたグローバル変数は、カプセル化する必要があります。 この変数はプライベートにする必要があり (ライブラリーのソース・コード の static キーワードで定義される)、アクセス (読み取りおよび書き込み) サブルーチンが作成されなければなりません。
  • 静的変数およびその他の共有リソースを識別する。 静的変数は、普通 static キーワードによって定義されます。 どの共有リソースにもロックを関連付ける必要があります。 ロックの細分性、すなわちロックの数の選択によって、 ライブラリーのパフォーマンスが影響を受けます。 ロックを初期化するには、ワンタイム初期化機能を使用することができます。
  • 再入不可関数を識別し、それらの関数を再入可能にする。 詳しくは、 関数を再入可能にするを参照してください。
  • スレッド・アンセーフ関数を識別し、それらの関数をスレッド・セーフにする。 詳しくは、 関数をスレッド・セーフにするを参照してください。