12    スレッド・セーフなライブラリの開発

マルチスレッド・アプリケーションの開発をサポートするために,Tru64 UNIX オペレーティング・システムでは,POSIX Threads Library (Compaq Multithreading Run-Time Library) を提供しています。 POSIX Threads Library インタフェースは,いくつかの拡張機能を加えた,IEEE 規格 1003.1c-1995 スレッド (POSIX P1003.1C スレッドとも呼ぶ) の Tru64 UNIX のインプリメンテーションです。

実際のスレッド・インタフェースのほかに,オペレーティング・システムでは TIS (Thread-Independent Services: スレッド独立型サービス) を提供しています。 TIS ルーチンは,独自のスレッドを作成しない効率的なスレッド・セーフ・ライブラリの作成を支援します (TIS ルーチンについての詳細は,12.4.1 項を参照)。

この章では,次の項目について説明します。

12.1    スレッド・サポートの概要

スレッドとは,プログラム内における単一のシーケンシャルな制御の流れのことです。 複数のスレッドが同時に実行され,アドレス空間をはじめ,所有しているプロセスのほとんどのリソースを共用します。 省略時の設定では,プロセスには最初に 1 つのスレッドがあります。

複数のスレッドは,次のような目的で使用されます。

また,複数のスレッドを特定のイベント管理の代替方法として使用することもできます。 たとえば,select() あるいは poll() システム・コールを使用して,複数のファイル記述子への同時 I/O オペレーションを管理する代わりに,各プロセスのファイル記述子ごとに 1 つのスレッドを使用することもできます。

Tru64 UNIX システムに対するマルチスレッド開発環境の構成要素は次のとおりです。

マルチスレッド・アプリケーションのプロファイリングについては,8.8 節を参照してください。

ロジックおよび性能の潜在的な問題について,マルチスレッド・アプリケーションを分析するには,Visual Threads (「Associated Products Volume 1」CD-ROM から利用可能) を使用することができます。 Visual Threads は,POSIX Threads Library アプリケーションおよび Java アプリケーションで使用することができます。

12.2    POSIX 準拠のための実行時ライブラリの変更

DEC OSF/1 オペレーティング・システム (DIGITAL UNIX Version 4.0 より古いバージョン) のリリースでは,別個のリエントラント・ルーチン (*_r ルーチン) を多数提供して,C 実行時ライブラリ内の静的データの問題 (最初の 2 つの問題については12.3.1 項を参照) を解決していました。 Tru64 UNIX オペレーティング・システムのリリースでは,静的データをスレッド固有のデータと置き換えることによって,リエントラントでないルーチンの静的データの問題を解決しています。 POSIX 1003.1c で指定する数個のルーチンを除き,Tru64 UNIX システムでは代替ルーチンは必要なく,バイナリ互換性のみのために保持されています。

POSIX 1003.1c で指定されている代替スレッド・セーフ・ルーチンは次の関数だけであり,スレッド・セーフなコードを記述する際には必要です。

asctime_r* ctime_r* getgrgid_r*
getgrnam_r* getpwnam_r* getpwuid_r*
gmtime_r* localtime_r* rand_r*
readdir_r* strtok_r  

DIGITAL UNIX バージョン 4.0 からは,前述の一覧でアスタリスク (*) の付いているインタフェースは,POSIX 1003.1c に準拠する新しい定義になっています。 これらのルーチンの旧バージョンは,プリプロセッサ・シンボル _POSIX_C_SOURCE に値 199309L (POSIX 1003.1b 準拠を示す -- ただし,これを行うと POSIX 1003.1c スレッドが無効になる) を指定して定義することにより取得できます。 これらのルーチンの新バージョンは,DIGITAL UNIX バージョン 4.0 以降でコードをコンパイルしたときの省略時の設定ですが,各ルーチンのリファレンス・ページで指定されているヘッダ・ファイルをインクルードすると確実です。

スレッドを使用してのプログラミングについては,『Guide to the POSIX Threads Library』 および cc(1)monitor(3)prof(1)gprof(1) を参照してください。

12.3    スレッド・セーフ・ルーチンおよびリエントラント・ルーチンの特性

ライブラリ内のルーチンは,スレッド・セーフであっても,スレッド・セーフでなくても構いません。 スレッド・セーフなルーチンは,複数のスレッドから,スレッド間の望ましくない相互作用なしで,同時に呼び出すことができるルーチンです。 ルーチンは,次のいずれかの理由によってスレッド・セーフのことがあります。

リエントラント関数は,複数のスレッドからの同時呼び出しで,状態を共用しません。 リエントラント・ルーチンは理想的なスレッド・セーフのルーチンですが,すべてのルーチンがリエントラントとして作成できるわけではありません。

DIGITAL UNIX バージョン 4.0 より前では,C 実行時ライブラリ (libc) ルーチンの多数はスレッド・セーフではなく,これらのルーチンの代替バージョンが libc_r で提供されていました。 DIGITAL UNIX バージョン 4.0 からは,以前 libc_r で提供されていたすべての代替バージョンは,libc にマージされました。 スレッド・セーフ・ルーチンとそれに対応するスレッド・セーフでないルーチンが同じ名前を持っている場合は,スレッド・セーフでないルーチンが置き換えられました。 スレッド・セーフのルーチンは TIS ルーチンを使用するように変更されています (12.4.1 項を参照)。 これは,シングル・スレッドの場合に広範囲にわたるオーバヘッドなしで,シングル・スレッドおよびマルチスレッドの両方の環境で動作します。

12.3.1    スレッド・セーフでないコーディング例

コードをスレッド・セーフにしないようにする方法は,DIGITAL UNIX バージョン 4.0 より前にスレッド・セーフでなかった libc 関数のいくつかを調べるとわかります。

12.4    スレッド・セーフ・コードの作成

シングル・スレッドおよびマルチスレッド・アプリケーションの両方で使用できるコードを作成するときには,スレッド・セーフな方法でコーディングする必要があります。 次のコーディング方法に従ってください。

12.4.1    スレッド固有データに対する TIS の使用

以下の項では,スレッド固有データに対して TIS (Thread Independent Services) を使用する方法について説明します。

12.4.1.1    TIS の概要

TIS (Thread Independent Services) は,C 実行時ライブラリによって提供されるルーチンのパッケージであり,シングル・スレッドおよびマルチスレッド・アプリケーションの両方に対して,効率的なコードを作成するために使用されます。 TIS ルーチンは,ミューテックスの処理,スレッド固有データの処理,およびその他のさまざまな目的で使用することができます。

シングル・スレッド・アプリケーションで使用されると,これらのルーチンは単純化された意味規則を使用して,シングル・スレッド用のスレッド・セーフ・オペレーションを実行します。 POSIX Threads Library が存在する場合は,ルーチン本体がより複雑なアルゴリズムで置き換えられて,マルチスレッド用に動作が最適化されます。

TIS を libc 自体の内部で使用すると,1 つのバージョンの C 実行時ライブラリが,シングル・スレッドおよびマルチスレッド・アプリケーションの両方で使用できるようになります。 この機能の使用方法についての詳細は,『Guide to the POSIX Threads Library』 および tis(3) を参照してください。

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

例 12-1 は,シングル・スレッドおよびマルチスレッド・アプリケーションの両方で使用できる関数でスレッド固有のデータを使用する方法を示しています。 簡潔にするため,ほとんどのエラー・チェックは省いています。

例 12-1:  スレッド・プログラム例

#include <stdlib.h>
#include <string.h>
#include <tis.h>
 
static pthread_key_t key;
 
void _ _init_dirname()
{
	tis_key_create(&key, free);
}
 
void _ _fini_dirname()
{
	tis_key_delete(key);
}
 
char *dirname(char *path)
{
	char *dir, *lastslash;
/*
 * Assume key was set and get thread-specific variable.
 */
	dir = tis_getspecific(key);
	if(!dir) {	/* First time this thread got here. */
		dir = malloc(PATH_MAX);
		tis_setspecific(key, dir);
	}
 
/*
 * Copy dirname component of path into buffer and return.
 */
	lastslash = strrchr(path, '/');
	if(lastslash) {
		memcpy(dir, path, lastslash-path);
		dir[lastslash-dir+1] = '\0';
	} else
		strcpy(dir, path);
	return dir;
}

次の TIS ルーチンが前述の例で使用されています。

tis_key_create

一意なデータ・キーを生成します。

tis_key_delete

データ・キーを削除します。

tis_getspecific

指定されたキーに関連するデータを取得します。

tis_setspecific

指定されたキーに関連するデータ値を設定します。

_ _init_ および _ _fini_ ルーチンは,この例ではスレッド固有のデータ・キーを初期化して破壊するために使用されています。 このオペレーションは 1 度だけ行われ,これらのルーチンは,ライブラリが dlopen() でロードされている場合にも,このことを確実に示す便利な方法を提供します。 _ _init_ および _ _fini_ ルーチンの使用方法についての説明は, ld(1) を参照してください。

スレッド固有のデータ・キーは,実行時に POSIX Threads Library によって提供される限定リソースです。 多数のデータ・キーを使用する必要のあるライブラリは,1 つのデータ・キーだけを作成して,別々のデータ項目をすべて,構造体あるいは,そのキーで指し示されるポインタの配列として保存するようにライブラリをコーディングします。

12.4.2    TLS (Thread Local Storage) の使用

C コンパイラでは,TLS (Thread Local Storage) のサポートは常に有効になっています (cc コマンドの -ms オプションは不要です)。 C++ では,TLS は -ms オプションを指定したときのみ認識され,指定していないときはエラーとして処理されます。

TLS は,マルチスレッド・プロセスのスレッドが存在する期間に静的エクステントを持ち (スタック上ではない),スレッドごとに割り当てられるデータ領域です。

標準のマルチスレッド・プログラムでは,静的エクステント・データは,プロセスのすべてのスレッドで共有されますが,TLS は各スレッドごとに割り当てられ,各スレッドにはそれぞれ独自にデータのコピーがあり,スレッドがそのデータを変更しても,プロセス内の他のスレッドから見える値には影響を与えないようになっています。 スレッドについての詳細は,『Guide to the POSIX Threads Library』 を参照してください。

TLS の主要な機能は,POSIX (POSIX Threads Library) の pthread_key_create()pthread_setspecific()pthread_getspecific()pthread_key_delete() のようなアプリケーション・プログラミング・インタフェース (API) によって提供されてきました。

これらの API は POSIX 準拠のプラットフォーム間での移植性がありますが,使いにくく間違いが起こりやすくなることがあります。 また,適切な static および extern 変数宣言とその使用をすべて,スレッド・ローカル API の呼び出しに置き換えて,既存のシングルスレッド・コードをスレッド・セーフにするには,通常,かなりの技術的作業が必要になります。 さらに,Windows-32 プラットフォームでは API のセット (TlsAlloc()TlsGetValue()TlsSetValue()TlsFree()) が少し異なっており,POSIX API の場合と同じような使用上の問題があります。

これに対して,TLS の言語機能はいずれの API よりも使い方が簡単で,シングルスレッド・コードをマルチスレッド・コードに変換する際は特に便利です。 これは,static または extern 変数がスレッド固有の値を持つように変更するには,宣言に記憶クラス修飾子を追加するだけでよいからです。 コンパイラ,リンカ,プログラム・ローダ,デバッガは,この修飾子で宣言された変数に対して,複雑な API 呼び出しを自動的に効率良く実現します。 API によるコーディングとは異なり,変数の使用をすべて探して変更したり,明示的に割り当ておよび割り当て解除コードを追加する必要はありません。 この言語機能は,正式なプログラミング標準では一般に移植性がありませんが,Tru64 UNIX と Windows-32 プラットフォームの間では移植性があります。

12.4.2.1    _ _thread 属性

Tru64 UNIX の C および C++ コンパイラには,拡張記憶クラス属性,_ _thread が含まれます。

スレッド変数を宣言するには,_ _thread 属性を _ _declspec キーワードとともに使用しなければなりません。 たとえば,次のコードは整数のスレッド・ローカル変数を宣言し,それを値で初期化しています。

_ _declspec( _ _thread ) int tls_i = 1;

12.4.2.2    ガイドラインと制限

スレッド・ローカルのオブジェクトおよび変数を宣言する際は,以下のガイドラインと制限を守らなければなりません。

12.4.3    スレッド間でデータを共用するためのミューテックス・ロックの使用

場合によっては,静的データをスレッド・セーフ・コードに変換するために,スレッド固有のデータを使用することは有効ではありません。 たとえば,データ・オブジェクトがスレッド間で共用される (libc 内の stdio ストリームのように) 場合には,スレッド固有のデータは使用すべきではありません。 プロセス毎のリソースの操作も,スレッド固有データが不適切な場合の例です。 次の例は,スレッド・セーフな方法でプロセス毎のリソースを操作する方法を示しています。

#include <pthread.h>
#include <tis.h>
 
/*
 * NOTE: The putenv() function would have to set and clear the
 * same mutex lock before it accessed the environment.
 */
 
extern char **environ;
static pthread_mutex_t environ_mutex = PTHREAD_MUTEX_INITIALIZER;
 
char *getenv(const char *name)
{
        char **s, *value;
        int len;
        tis_mutex_lock(&environ_mutex);
        len = strlen(name);
        for(s=environ; value=*s; s++)
                if(strncmp(name, value, len) == 0 &&
                   value[len] == '=') {
                        tis_mutex_unlock(&environ_mutex);
                        return &(value[len+1]);
                }
        tis_mutex_unlock(&environ_mutex);
        return (char *) 0L;
}
 

この例では,環境にアクセスする前にロックが 1 度設定され (tis_mutex_lock),リターンする前に 1 度だけロックが解除されている (tis_mutex_unlock) ことに注意してください。 マルチスレッドの場合には,最初のスレッドがロックを保持している間に他のスレッドがその環境にアクセスしようとすると,最初のスレッドがロック解除を実行するまで,他のスレッドはブロックされます。 シングル・スレッドの場合には,ロックとロック解除のシーケンスにコーディング・エラーが存在しない限り,競合は起こりません。

マルチスレッド・アプリケーションで,ロック状態を fork() システム・コールの呼び出し中にも有効なままにしておく必要がある場合は,pthread_atfork() ハンドラ関数を作成および登録して,fork() 呼び出しの前にそのロックを設定し,fork() 呼び出しの後で子および親の両方でそのロックを解除する方法が有効です。 これにより,別のスレッドがロックを保持している間に,別のスレッドがフォーク操作を行うことがなくなります。 ロックが別のスレッドによって保持されている場合には,フォーク操作によって 1 つのスレッドだけを持つ子が作成されるため,子で永久にロックされることになります。 独立したライブラリの場合には,pthread_atfork() への呼び出しは,そのライブラリの _ _init_ ルーチンで行われます。 ほとんどの Pthread ルーチンと異なり,pthread_atfork ルーチンは libc で使用可能であり,シングル・スレッドおよびマルチスレッド・アプリケーションの両方で使用することができます。

12.5    マルチスレッド・アプリケーションの作成

マルチスレッド・アプリケーションのコンパイルおよびリンクは,シングル・スレッド・アプリケーションのコンパイルおよびリンクとは多少異なります。 以下の項では,この違いについて説明します。

12.5.1    マルチスレッド C アプリケーションのコンパイル

アプリケーションがシングル・スレッドかあるいはマルチスレッドかによって,多くのシステム・ヘッダ・ファイルは,アプリケーションのコンパイルでインクルードされる際に,異なる定義のセットを提供します。 コンパイラが,シングル・スレッドあるいはスレッド・セーフのどちらの動作を生成するかは,プリプロセッサ・シンボル _REENTRANT が定義されているかどうかによって決まります。 cc または c89 コマンドに -pthread オプションを指定すると,_REENTRANT シンボルが自動的に定義されます。 また,pthreads.h ヘッダ・ファイルがインクルードされている場合も定義されます。 Pthread ライブラリ libpthread.so を使用するアプリケーションでは,このヘッダ・ファイルを最初にインクルードする必要があります。

-pthread オプションは,C プログラムのコンパイルに対してはその他の影響は与えません。 C コンパイラによって作成されるコードのリエントラント性は,特定のオプションではなく,プログラマによる適切なリエントラント・コーディングの使用,スレッド・セーフ・サポート・ルーチンおよび関数のみの使用によって決まります。

12.5.2    マルチスレッド C アプリケーションのリンク

マルチスレッド C アプリケーションをリンクする場合は,-pthread オプションを指定して cc または c89 コマンドを使用します。 -pthread オプションは,リンク時に次の方法でライブラリ探索パスの修正に影響を与えます。

-pthread オプションは,リンカの動作に対してはその他の影響は与えません。 リンクされたコードのリエントラント性は,元のコードにおける適切なリエントラント・コーディングの使用,あるいは適切なヘッダ・ファイルあるいはライブラリによるコンパイルおよびリンクによって決まります。

12.5.3    その他の言語のマルチスレッド・アプリケーションの作成

すべてのコンパイラがリエントラント・コードを生成するとは限りません。 言語によっては困難な場合もあります。 また,アプリケーションにリンクされる実行時ライブラリがすべてスレッド・セーフであることも必要となります。 詳細については,使用するコンパイラのマニュアルおよび実行時ライブラリのマニュアルを参照してください。