この章では、関数とライブラリについて、マルチスレッドに対する安全レベルを定義します。
「スレッド安全」とは、データアクセスの競合 (つまり、複数のスレッドがデータをアクセスして変更するときに、その順番によってデータの値が正しくなったり正しくなくなったりする状況) を回避することです。
スレッド間でデータを共有する必要がない場合は、スレッドごとに専用のコピーを与えますが、共有する必要がある場合には、明示的に同期をとることによってプログラムが確定的な動きをするように制御する必要があります。
手続きが「スレッド安全」とは、その手続きが複数のスレッドによって同時に実行されても論理的な正しさが失われないことです。実際は、安全性は次の 3 段階で区別されます。
「スレッド安全ではない」
「スレッド安全」− 直列化
「スレッド安全」− MT-安全
「スレッド安全ではない」手続きであっても、mutex をロックする命令と解除する命令で囲めば、その処理は直列化され「スレッド安全」になります。例 6-1 は fputs() を簡略化したもので、最初のルーチンは「スレッド安全ではない」例です。
2 番目のルーチンは直列化した例です。ここでは、1 つの mutex で手続きを並行実行させないようにしています。これは通常必要とされる同期よりも強い同期となります。2 つのスレッドが fputs() を使って異なるファイルに出力するときは、一方がもう一方を待たせる必要はありません。両者の間で同期をとる必要があるのは、同じ出力ファイルを共有しているときだけです。
最後のルーチンは、「MT-安全」の例です。ここではファイルごとに mutex をロックしているので、2 つのスレッドが異なるファイルに同時に出力できます。つまり、ルーチンが「MT-安全」であるとは、「スレッド安全」で、しかもそのルーチンの実行が性能に悪影響を及ぼさないことを意味します。
/* スレッド安全ではない */ fputs(const char *s, FILE *stream) { char *p; for (p=s; *p; p++) putc((int)*p, stream); } /* 直列化 */ fputs(const char *s, FILE *stream) { static mutex_t mut; char *p; mutex_lock(&m); for (p=s; *p; p++) putc((int)*p, stream); mutex_unlock(&m); } /* MT-安全 */ mutex_t m[NFILE]; fputs(const char *s, FILE *stream) { static mutex_t mut; char *p; mutex_lock(&m[fileno(stream)]); for (p=s; *p; p++) putc((int)*p, stream); mutex_unlock(&m[fileno(stream)]0; } |
『man pages section 3: Threads and Realtime Library Functions』の スレッドについて、インタフェースのスレッドサポートの安全レベルのカテゴリを表 6-1 にリストしています。 (これらのカテゴリの詳細は、Intro(3) のマニュアルページを参照してください)
表 6-1 インタフェースの安全レベル
カテゴリ |
説明 |
---|---|
Safe「安全」 |
このコードをマルチスレッドアプリケーションから呼び出しても安全 |
Safe with exceptions「例外付きで安全」 |
例外の内容については、マニュアルページの「注意事項 (NOTES)」の節を参照 |
Unsafe「安全ではない」 |
このインタフェースをマルチスレッドアプリケーションで使用するのは危険。ただし、複数のスレッドが、ライブラリ内で同時に実行されないようにアプリケーション側が対応すれば使用できる |
MT-Safe「MT-安全」 |
このインタフェースは、マルチスレッドアクセスに完全に対応している。つまり、安全であると同時に並行性もサポートしている |
MT-Safe with exceptions「例外付きで MT-安全」 |
例外については、『man pages section 3』の「注意事項 (NOTES)」の節を参照 |
このルーチンをシグナルハンドラから安全に呼び出すことができる。「非同期シグナル安全」ルーチンは、シグナルが割り込んでも自己デッドロックにならない |
|
Fork1-Safe「fork1-安全」 |
このときインタフェースは、Solaris の fork1(2) または POSIX の fork(2) が呼び出されたときに、保持していたロックを解放する |
『man pages section 3』からのインタフェースの安全レベルについては付録 C 「「MT-安全」ライブラリインタフェース」 を参照してください。該当するマニュアルページを参照してレベルを確認してください。
次の理由により安全化されていない関数もあります。
その関数を「MT-安全」にすると、シングルスレッドアプリケーションの性能に悪影響を及ぼす。
その関数が、安全ではないインタフェースを持っている。たとえば、スタックに確保したバッファへのポインタを戻すような関数です。こうした関数には、リエントラント (再入可能) な代替関数が用意されている場合があります。オリジナルの関数名の末尾に「_r」が付いているのがリエントラントな関数です。
関数名の末尾に「_r」が付いていない関数がマルチスレッドに対して安全かどうかは、マニュアルページを参照してください。「MT-安全」ではないことが明記されている関数は、同期機構で保護するか、初期スレッド以外では使用しないでください。
危険なインタフェースをもつ多くの関数には、「MT-安全」な代替関数が用意されています。これらの関数は、オリジナルの関数名の末尾に「_r」を付けることで区別されます。Solaris 環境に用意されている「_r」ルーチンを表 6-2 に示します。
表 6-2 リエントラント関数
asctime_r(3c) |
gethostbyname_r(3n) |
getservbyname_r(3n) |
ctermid_r(3s) |
gethostent_r(3n) |
getservbyport_r(3n) |
ctime_r(3c) |
getlogin_r(3c) |
getservent_r(3n) |
fgetgrent_r(3c) |
getnetbyaddr_r(3n) |
getspent_r(3c) |
fgetpwent_r(3c) |
getnetbyname_r(3n) |
getspnam_r(3c) |
fgetspent_r(3c) |
getnetent_r(3n) |
gmtime_r(3c) |
gamma_r(3m) |
getnetgrent_r(3n) |
lgamma_r(3m) |
getauclassent_r(3) |
getprotobyname_r(3n) |
localtime_r(3c) |
getauclassnam_r(3) |
getprotobynumber_r(3n) |
nis_sperror_r(3n) |
getauevent_r(3) |
getprotoent_r(3n) |
rand_r(3c) |
getauevnam_r(3) |
getpwent_r(3c) |
readdir_r(3c) |
getauevnum_r(3) |
getpwnam_r(3c) |
strtok_r(3c) |
getgrent_r(3c) |
getpwuid_r(3c) |
tmpnam_r(3s) |
getgrgid_r(3c) |
getrpcbyname_r(3n) |
ttyname_r(3c) |
getgrnam_r(3c) |
getrpcbynumber_r(3n) |
|
gethostbyaddr_r(3n) |
getrpcent_r(3n) |
「非同期シグナル安全」関数とは、シグナルハンドラから安全に呼び出すことができる関数のことです。それらは、POSIX 規格「IEEE Std 1003.1-1990, 3.3.1.3 (3)(f)」の 55 ページで定義されています。POSIX 規格の「非同期シグナル安全」関数に加え、スレッドライブラリの次の 3 つの関数も「非同期シグナル安全」関数です。
マルチスレッドプログラムから呼び出される可能性のあるルーチンは、どれも「MT-安全」であるべきです。
つまり、同時に呼び出される可能性のあるルーチンは、並行実行されても正しく実行されることが必要です。このため、マルチスレッドプログラムで使用するすべてのライブラリインタフェースは、「MT-安全」でなければなりません。
現状では、すべてのライブラリが「MT-安全」ではありません。代表的な「MT-安全」ライブラリを表 6-3 に示します。その他のライブラリも、最終的には「MT-安全」なものに修正されます。
表 6-3 「MT-安全」なライブラリの例
「MT-安全」であることが保証されていないライブラリのルーチンを、マルチスレッドプログラムから安全に呼び出すためには、それらの呼び出しがシングルスレッドで行われるようにしなければなりません。