2007年02月15日
PTHREAD_MUTEX_ERRORCHECK_NP
pthreadのmutexではpthread_mutex_initでattributeが付けられますが、PTHREAD_MUTEX_ERRORCHECK_NPというattributeが有るのを見つけたので適当に浅追いしてみました。
PTHREAD_MUTEX_ERRORCHECK_NPというのは、同じスレッドが同じmutexを2回ロックしようとした時に2回目のロックを明示的に失敗させたい時に指定するmutexのattributeです。その際、lock関数の返り値はEDEADLKという値になっています(成功時は0)。
_NPというsuffixはPOSIX互換で無い事を示しており、移植性があるプログラムでは使うべきではないです。移植性が無い代わりに、デバッグ時に結構便利ではないかなと思いちょっと調べてみました。
マルチスレッドプログラムが複雑になって細かく関数に分けていくと、ロックの取り方も複雑になってしまい、親関数でロックしたmutexを子関数でロックしてしまうことが良くあります。こういったケースでは下のようなマクロでラップしておく事で開発効率が上がるのでは無いかと思った次第です。
#include <pthread.h>
#include <errno.h>
#include <stdio.h>
pthread_mutex_t mutex;
#define LOCK(m) \
do { \
int r = pthread_mutex_lock(&(m)); \
if (r != 0) { \
if (r == EDEADLK) { \
printf("Deadlock detected: %s: %d\n", __FILE__, __LINE__); \
} \
} \
} while (0)
#define UNLOCK(m) \
do { \
pthread_mutex_unlock(&(m)); \
} while (0)
void func2()
{
LOCK(mutex);
pthread_mutex_unlock(&mutex);
UNLOCK(mutex);
}
void func1()
{
LOCK(mutex);
func2();
UNLOCK(mutex);
}
int main(void)
{
pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ERRORCHECK_NP);
// pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init(&mutex, &mutex_attr);
func1();
}
EDEADLKという名前のわりにerrnoの値ではない所に少しはまりました。
実行結果。
[kzk@pfidev1]~% gcc -D_REENTRANT -lpthread edeadlk.c; ./a.out Deadlock detected: edeadlk.c: 24
でもいくつかの点で、導入を躊躇わせる点があります。
(1) RAIIと相性が良くない
C++をメインで使っているのでロック周りはRAIIのお世話になっているわけですが、これだとデッドロックしたのは分かっても、マクロにしてファイルと行数を表示させられないとあまり意味が無いですね。
逆にPure Cだとpthread_mutex_lockを全てLOCKマクロに置き換える必要がある。
(2) mutexの初期化
全てのmutexの初期化コードに2, 3行追加しないといけないですし、またPTHREAD_MUTEX_INITIALIZERをPTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NPに置き換える必要も有ります。
うだうだと長くなってしまいましたが、結局ロック周りのコードをほぼ書き換えてまで導入する場面は少なそうだという結論になりました。このattributeで見つけられるデッドロックは、デバッグ段階で簡単に発現するし、それを解決しないとプログラムは全然動かないからです。
むしろとりあえず動いてから稀に発言するようなDeadlockを見つけてくれるようなツール or 手法が欲しいところです。
P.S.
少しだけ中身も見てみるかということで、glibc-2.5/nptl/pthread_mutex_lock.cの__pthread_mutex_lock関数を見てみると以下のようなコードが有りました。
/* Error checking mutex. */
case PTHREAD_MUTEX_ERRORCHECK_NP:
/* Check whether we already hold the mutex. */
if (__builtin_expect (mutex->__data.__owner == id, 0))
return EDEADLK;
....
/* Record the ownership. */
mutex->__data.__owner = id;
単純にowner threadのIDを覚えているだけみたいです。
- by
- at 01:52

comments
helgrindって、dead lockの検出はできなかったんでしたっけ?
そう言えば、Linuxのmutex_debugに入っているような関数群がlibcにも付いていると便利じゃないかと思うんですけど、誰かやらないですかね。
hellgrindはdata raceを見つけてくれるのですが、アウトプットする情報がいまいちでした。ためしに下のような2つの関数をそれぞれのスレッドでまわしてみました。
void* func1(void *) { while (true) { pthread_mutex_lock(&mutex1); pthread_mutex_lock(&mutex2); cnt++; pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); } }void* func2(void *)
{
while (true) {
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
cnt++;
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
}
}
するとこんな感じのアウトプットが得られます。
この場合はプログラム規模が小さいので正確に場所を出してくれていますねぇ。
ただもっと規模が大きくなると、glibcやnptl内のdata raceの場所のアウトプットが大半をしめて、ユーザープログラム内の問題が埋もれてしまったというのが現状でした。
特定の関数についての出力を設定ファイルによって抑制するような方法もあるんですが、pthread_*をリストするのもなんか間違ってるような気がするんですよね・・・。
> そう言えば、Linuxのmutex_debugに入っているような関数群がlibcにも付いていると便利じゃないかと思うんですけど、誰かやらないですかね。
こんなのが有るんですね。ちょっとコード見てみてnptlに適用出来ないかみてみます。情報有難うございますm(_ _)m