pthread プログラミング要項

SUSv4 (POSIX 2008)を参照する Debian 6.0 (squeeze)用の改訂版 があるよ。


pthread プログラミングの要点と注意事項。 なるべく POSIX 準拠の範囲内でプログラムを書くために。

1. pthread プログラミング要項

2. 注意事項

3. 参考


1. pthread プログラミング要項

1.1 準備

資料

まずは apt-get install manpages-posix manpages-posix-dev

環境はコマンド getconf で確認する。

% getconf GNU_LIBPTHREAD_VERSION
NPTL 2.3.6

この場合、 LinuxThreads ではなく NPTL を使う環境になっている(はず)。 pthreads (7) に pthread 実装の概要がある。

マニュアルには LinuxThreads の場合の言及も多い。 微妙な点は 3posix セクションも見ておく方がよい。

オプション機能

getconf に尋ねると

% getconf _XOPEN_UNIX
1

なのでスレッド関連の _POSIX_THREAD_ATTR_STACKADDR _POSIX_THREAD_ATTR_STACKSIZE _POSIX_THREAD_PROCESS_SHARED _POSIX_THREAD_SAFE_FUNCTIONS _POSIX_THREADS はどれも使える。 実際に getconf は 200112 を返す。

次のオプションも 200112 を返した。 _POSIX_READER_WRITER_LOCKS _POSIX_THREAD_PRIORITY_SCHEDULING _POSIX_BARRIERS _POSIX_SPIN_LOCKS

次のオプションは undefined を返した。 _POSIX_THREAD_CPUTIME _POSIX_THREAD_SPORADIC_SERVER _POSIX_THREAD_PRIO_INHERIT _POSIX_THREAD_PRIO_PROTECT

用法

#include <pthread.h> して記述し、 -lpthread でリンクする。 実時間拡張のオプションを使う場合もこれだけでよい。 -lrt は必要ない。

XSI (X/Open System Interfaces)のオプション(_POSIX_READER_WRITER_LOCKS や _POSIX_BARRIERS など)に関係する機能を使う場合、 コンパイル時にオプション -D_POSIX_C_SOURCE=200112L-D_XOPEN_SOURCE=600 を指定する。

マルチスレッドプログラムの実行状況は psm, H などで確認できる。 ps xH など。

1.2 インタフェース概観

pthread ライブラリは概ね次のような決まりに則る。

資源名

属性の指定

その他

1.3 基本操作

スレッドの作成と合流

同期

同期の道具として mutex, 条件変数、バリア、セマフォなどが使える。

セマフォ

ここでのセマフォは SysV IPC ではなく POSIX (多分実時間(REALTIME)拡張の一部)のセマフォ。 sem_overview (7) 参照。

pthread とは独立した規格だと思うが、リンク時には -lpthread を使える。

その他

他の同期機構

2. 注意事項

信頼性の高い多重スレッド処理を記述するために知っておく必要がありそうな事柄。

2.1 メモリ操作

複数スレッド(プロセスも)がメモリを共有する場合には メモリ同期の問題(あるスレッドが値を更新した直後は 別スレッドは変更前の値しか見えない事がある)に配慮を要する。

メモリ同期

結論

複数スレッドが共有の変数にアクセスする場合については、次の事を心得ておく。

なので、mutex かセマフォを使って排他処理すれば 別途メモリ同期処理は必要ない。 POSIX 準拠のシステムでは、通常のメモリアクセスであれば volatile 修飾も不要。 逆に、他にメモリ同期の手段は提供されないようなので POSIX の範囲内ではこれらの関数を使う以外に手はない。

背景

これらの問題は、メモリをアトミックに操作するとか volatile 修飾することでは本質的には防げない。 メモリバリア(メモリアクセスの順序を保護する命令)を使う必要があるだろう。

C(99) の volatile は、コードの記述に応じた順序で愚直に 逐一メモリにアクセスすることを促す。 しかし、多重スレッドやマルチ CPU を想定しておらず、 メモリバリアを配したコードを生成する保証はないらしい。

アトミック操作

厳格な競合の排除が不要な (更新途中の壊れた値を扱うことを避けらればよい)場合は、 メモリのアトミック操作で間に合う事もあるだろう。

メモリの参照から更新までをアトミックに処理するなら 単一 CPU の場合はメモリ同期の問題もないと考えられる。

CPU が複数ある場合を考えると、 更新時に排他用の命令(IA32 なら LOCK 命令を使うと SMP でも機能する?)を併用した アトミック操作を使う必要があるだろう。

アーキテクチャなどに依存しそうだけども、

2.2 スレッド安全性

POSIX の標準関数は大部分がスレッド安全(複数スレッドが並行して呼び出せる)。 例外は XSH 2.9.1 Thread-Safety に掲載されている 90 程度の関数。 その多くは、静的に割り当てたメモリを扱うことから推測できる。 いくつかの安全でない関数には、関数名末尾に _r が付いたスレッド安全な関数が提供される。 多分再入可能の r だと思うが、規格はスレッド安全性しか保証しないようだ。

2.3 非同期処理の回避

非同期処理の難しさ

シグナル処理やスレッドの取り消しの際にはハンドラが呼ばれる。 この関数が非同期に(スレッドの実行状況に関係なく)呼ばれる場合には 様々な制限がある。

fork() の子プロセス用のハンドラも非同期呼出と同様の問題を抱える。

atexit() の登録関数は exit() を呼んだスレッドで同期的に実行されるのだと思う。 通常のスレッド安全性だけ考慮しておけばよいだろう。

非同期処理の回避

非同期処理を避けるため、次の原則に従うとよい。

典型的なシグナル処理

マルチスレッドプログラムでは、 次の処理をすれば非同期でのシグナル処理を避けられる。

  1. スレッド作成前(単一スレッド時)に sigprocmask() か pthread_sigmask() で扱いたいシグナルをブロックする。
  2. シグナル待機専用のスレッドを起こし、そこで sigwait() する。

    後は、 sigwait() が返ってきたらシグナル処理をすればよい。 通常通りにプログラミングできる。

sigwait の同類には sigtimedwait, sigwaitinfo がある。 これらならシグナルの詳しい情報(siginfo_t)も得られる。 sigwait 類は sigaction() 系と同時併用はできない。

2.4 非同期処理

非同期に呼ばれるハンドラを書く場合は、色々な事柄を知っておく必要がある。

再入可能性

再入可能性は、スレッド安全なだけでなく、 割り込み処理でも(割り込んでも割り込まれても両方でも) 安全に呼び出せる事を指すようだ。

静的な内部状態(大域変数も)を扱わず、引数のみを使って処理する関数が相当する。 mutex やセマフォで排他制御する関数はスレッド安全であっても再入不可能だろう。 再入不可能な関数を呼び出す関数は再入不可能になる。

関数 f が mutex を確保している状態で捕捉関数が呼ばれた場合、 捕捉関数が f を呼び出すとデッドロックしてしまう。

errno の扱いは微妙。 通常のシステムコールを使えば errno を変更する可能性がある。 システムコールを呼ぶだけで 厳密には再入不可能の烙印を押されることになろう。

最初に errno の値を調べて何かをするような 割り込み処理はないはず (errno の値が壊れていない保証がないので参照してもしかたない)なので、 ハンドラが errno 一時的に保管しておいて return 前に復旧すれば実用上は困らないだろう (厳密には errno へのアクセス自体を保証されないだろうから 保管、復旧できる保証もない)。

シグナル処理の背景

シグナルはスレッド単位でマスクできるが、 捕捉関数はプロセス単位でしか指定できない。

非同期シグナル処理

POSIX.1 の XSH 2.4 Signal Concepts に関係する部分(多重スレッド特有の話に限らない)。

非同期シグナル安全な標準関数は、 signal (2) の NOTES の項に一覧(POSIX.1-2003)がある(全117関数)。 これらは再入可能な関数とシグナル割り込み不可能な関数。 POSIX.1 の XSH (2004) 2.4.3 Signal Actions は sockatmark() を含む 118 関数。

一覧を眺めると、

これら以外の関数(以下、不用心関数)はシグナル安全性は保証されない。 なのでシグナル捕捉関数が printf() を呼ぶだけで アプリケーションの振舞いは予測できなくなる。

捕捉関数に課される制限

一般に捕捉関数(非同期な呼出で)が次の処理をした結果は undefined ( sigaction (3posix) の APPLICATION USAGE の項).

a. については次の記述がある (2.4.3 Signal Actions の非同期シグナル安全関数一覧の後)。

要するに、不用心関数が不用心関数に割り込まなければよいと。

b. については、 volatile sig_atomic_t 型の変数への「代入」ならば大丈夫。

なので非同期シグナル安全な関数(例: pause, sigsuspend) でシグナル待つ場合は、色々と関数が呼べるようだ。 でも普通の大域変数を扱えない。 sigsuspend() で窮屈(もしくは POSIX 保証外の危険な)処理をするよりは sigwait() で同期処理する方が易しい。

多重スレッドでの fork()

fork() で作られた子プロセスは単一スレッド。 fork() を呼んだスレッドだけが動いており、他のスレッドは動いていない。

fork() 以外のスレッドは動いていないので、 write() を呼び出す直前のスレッドがあっても、 それが2重化されて同じデータが二度書かれるといった問題は起こらない。

fork ハンドラ

fork に際して mutex の解放処理などが必要なら pthread_atfork (3) で ハンドラを登録しておく。 この関数には、親プロセスの fork() 前後と子プロセスとで呼ばれる3種類の関数を 渡す。

想定している使い方は、

  1. fork 前に必要な mutex を全部獲得する。
  2. fork 後に獲得した mutex を解放する(親子両プロセスで)。

3. 参考

3.1 論拠、出典

規格書

undefined

Describes the nature of a value or behavior not defined by IEEE Std 1003.1-2001 which results from use of an invalid program construct or invalid data input.

undefined

Describes the nature of a value or behavior not specified by IEEE Std 1003.1-2001 which results from use of a valid program construct or valid data input.

2004 ではなく 2001 と規定しているのは意図的?

メモリ同期 - 出典

メモリ同期については POSIX.1 XBD 4.10 Memory Synchronization で規定している。

Such access is restricted using functions that synchronize thread execution and also synchronize memory with respect to other threads.
そういったアクセス(その箇所を変更する別のスレッド/プロセスがある メモリへのアクセス)は、スレッドの実行を同期化し、他のスレッドに対して メモリを同期化する関数を使う制限が課される。

C99 volatile

6.7.3 Type qualifiers

An object that has volatile-qualied type may be modied in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modied by the unknown factors mentioned previously.119) What constitutes an access to an object that has volatile-qualied type is implementation-dened.

119) A volatile declaration may be used to describe an object corresponding to a memory-mapped input/output port or an object accessed by an asynchronously interrupting function. Actions on objects so declared shall not be optimized out by an implementation or reordered except as permitted by the rules for evaluating expressions.

3.2 疑問・未決

シグナルの同期/非同期

同期/非同期シグナルの定義が不明瞭。 The Open Group Base Specifications Issue 6 の 3.27 Asynchronously-Generated Signal では、

A signal that is not attributable to a specific thread. Examples are signals sent via kill(), signals sent from the keyboard, and signals delivered to process groups. Being asynchronous is a property of how the signal was generated and not a property of the signal number.

スレッド実行中に raise(), abort() した場合は同期だろう。 kill() も自スレッド宛なら同期だろうが、 別スレッド/プロセスから送られた場合は非同期だと思う。

SIGSEGV や SIGBUS が(意図せずに)発生した場合は、 同期/非同期の諸説があるように思える。

POSIX の signal の説明には次の記述がある。

If the signal occurs other than as the result of calling abort(), raise(), kill(), pthread_kill(), or sigqueue(), the behavior is undefined if the signal handler refers to any object with static storage duration other than by assigning a value to an object declared as volatile sig_atomic_t, or if the signal handler calls any function in the standard library other than one of the functions listed in Signal Concepts. Furthermore, if such a call fails, the value of errno is unspecified.

kill() でシグナルを送った場合は問題がないと受け取れる。 受け手のスレッドの間合いを見計らってシグナルを届けるというのは考えにくいが。 別スレッドに送った場合を除外している?

シグナル不用心関数を呼んだ場合の振舞い

2.4.3 Signal Actions には、 非同期シグナルで呼ばれた捕捉関数がシグナル不用心関数を 呼び出した場合の振舞いは不定(unspecified)ともある。

unspecified と言いながら、入れ子になった場合だけ undefined で その他は仕様通りに振舞うと。

ロックなしで条件通知してよいか?

条件通知 pthread_cond_signal() をとりこぼしても困らない場合がある。 一般に、こういった場合に 排他制御(mutex)なしで pthread_cond_signal() で通知してもいいか?

POSIX のマニュアルには、明確に mutex は不要と書いてある。 pthread_cond_signal (3posix) 曰く、

The pthread_cond_broadcast() or pthread_cond_signal() functions may be called by a thread whether or not it currently owns the mutex that threads calling pthread_cond_wait() or pthread_cond_timedwait() have associated with the condition variable during their waits; however, if predictable scheduling behavior is required, then that mutex shall be locked by the thread calling pthread_cond_broadcast() or pthread_cond_signal().

一つの条件変数について複数のスレッドが各個別の mutex を伴って 待機する事を許すのなら、 pthread_cond_signal() は mutex 無しで呼べる必要があるだろう。

そもそも、 pthread_cond_signal() は mutex を引数に取るわけではない。 その場合に mutex が必要なのなら引数にとって然るべき?

引数の pthread_cond_t * は const 修飾されているわけでもない。 pthread_cond_signal() が引数の中身を変更する可能性はあるが、 その際、排他的に処理する実装であることが求められている?

最も気になるのは、 pthread_cond_signal (3) の次の記述

A condition variable must always be associated with a mutex, to avoid the race condition where a thread prepares to wait on a condition vari- able and another thread signals the condition just before the first thread actually waits on it.
これは、合図を受け取り損なわないように mutex を使えということで、 受け取り損ねて構わない場合については規定していないとも解釈できる。

このマニュアルページは LinuxThreads の場合の説明だろう。

また、 LinuxThreads では spurious wakup は無いようだ。 NTPL だとあるのかもしれない。


目録 / 戻る $Id: index.html 2008/08/21 01:21:46 $ [泉 謹製]