PTHREAD_BARRIER_WAITが戻るとすぐに、どのように障壁を破壊可能にすることができますか?
-
28-10-2019 - |
質問
この質問は次のものに基づいています。
そして最近のGLIBCバグレポート:
http://sourceware.org/bugzilla/show_bug.cgi?id=12674
GLIBCで報告されているセマフォの問題についてはわかりませんが、おそらくすぐに障壁を破壊することが有効であるはずです pthread_barrier_wait
上記のリンクされた質問に従って、戻ります。 (通常、得たスレッド PTHREAD_BARRIER_SERIAL_THREAD
, 、または、バリアオブジェクトに対してすでに「責任」と見なされている「特別な」スレッドは、それを破壊するものです。)私が考えることができる主なユースケースは、新しいスレッドのデータの使用を同期するために障壁を使用している場合です作成されたスレッドのスタックでは、新しいスレッドがデータを使用するまで作成されたスレッドが戻るのを防ぎます。他の障壁は、おそらくプログラム全体の寿命と同等の寿命を持っているか、他の同期オブジェクトによって制御されます。
いかなる場合でも、 どうやって 実装により、障壁の破壊(そしておそらくそれが存在するメモリのマッピングさえも)がすぐに安全であることを保証できますか? pthread_barrier_wait
どんなスレッドでも返されますか?まだ返されていない他のスレッドは、上記のGlibcバグレポートで、どのようにして作業を終了して戻ってくるために、バリアオブジェクトの少なくとも一部を調べる必要があるようです。 sem_post
セマフォの値を調整した後、ウェイターカウントを調べる必要があります。
解決
の実装の例で、これで別の亀裂を取るつもりです pthread_barrier_wait()
これは、PTHREADS実装によって提供される可能性のあるMutexおよび条件変数機能を使用します。この例は、パフォーマンスの考慮事項に対処しようとしないことに注意してください(具体的には、待機スレッドがブロックされていない場合、待機を終了するときにすべて再セリア化されます)。 Linux Futexオブジェクトのようなものを使用すると、パフォーマンスの問題に役立つ可能性がありますが、Futexesはまだ私の経験からはほとんどありません。
また、この例が信号またはエラーを正しく処理していることは疑わしい(信号の場合はまったく場合)。しかし、これらのものに対する適切なサポートは、読者のための演習として追加できると思います。
私の主な恐れは、この例には人種の状態またはデッドロックがあるかもしれないということです(ミューテックスの取り扱いは私が好むよりも複雑です)。また、コンパイルされていない例であることにも注意してください。それを擬似コードとして扱います。また、私の経験は主にWindowsにあることに留意してください - 私はこれを他の何よりも教育的な機会として取り組んでいます。したがって、擬似コードの品質はかなり低いかもしれません。
しかし、免責事項はさておき、質問で尋ねられた問題をどのように処理できるかについてのアイデアを与えるかもしれないと思います(つまり、どうやって pthread_barrier_wait()
関数許可 pthread_barrier_t
オブジェクトは、バリアオブジェクトを1つ以上のスレッドで使用する途中で使用する危険なしに、放出されたスレッドのいずれかによって破壊されるために使用されます)。
ここにあります:
/*
* Since this is a part of the implementation of the pthread API, it uses
* reserved names that start with "__" for internal structures and functions
*
* Functions such as __mutex_lock() and __cond_wait() perform the same function
* as the corresponding pthread API.
*/
// struct __barrier_wait data is intended to hold all the data
// that `pthread_barrier_wait()` will need after releasing
// waiting threads. This will allow the function to avoid
// touching the passed in pthread_barrier_t object after
// the wait is satisfied (since any of the released threads
// can destroy it)
struct __barrier_waitdata {
struct __mutex cond_mutex;
struct __cond cond;
unsigned waiter_count;
int wait_complete;
};
struct __barrier {
unsigned count;
struct __mutex waitdata_mutex;
struct __barrier_waitdata* pwaitdata;
};
typedef struct __barrier pthread_barrier_t;
int __barrier_waitdata_init( struct __barrier_waitdata* pwaitdata)
{
waitdata.waiter_count = 0;
waitdata.wait_complete = 0;
rc = __mutex_init( &waitdata.cond_mutex, NULL);
if (!rc) {
return rc;
}
rc = __cond_init( &waitdata.cond, NULL);
if (!rc) {
__mutex_destroy( &pwaitdata->waitdata_mutex);
return rc;
}
return 0;
}
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count)
{
int rc;
result = __mutex_init( &barrier->waitdata_mutex, NULL);
if (!rc) return result;
barrier->pwaitdata = NULL;
barrier->count = count;
//TODO: deal with attr
}
int pthread_barrier_wait(pthread_barrier_t *barrier)
{
int rc;
struct __barrier_waitdata* pwaitdata;
unsigned target_count;
// potential waitdata block (only one thread's will actually be used)
struct __barrier_waitdata waitdata;
// nothing to do if we only need to wait for one thread...
if (barrier->count == 1) return PTHREAD_BARRIER_SERIAL_THREAD;
rc = __mutex_lock( &barrier->waitdata_mutex);
if (!rc) return rc;
if (!barrier->pwaitdata) {
// no other thread has claimed the waitdata block yet -
// we'll use this thread's
rc = __barrier_waitdata_init( &waitdata);
if (!rc) {
__mutex_unlock( &barrier->waitdata_mutex);
return rc;
}
barrier->pwaitdata = &waitdata;
}
pwaitdata = barrier->pwaitdata;
target_count = barrier->count;
// all data necessary for handling the return from a wait is pointed to
// by `pwaitdata`, and `pwaitdata` points to a block of data on the stack of
// one of the waiting threads. We have to make sure that the thread that owns
// that block waits until all others have finished with the information
// pointed to by `pwaitdata` before it returns. However, after the 'big' wait
// is completed, the `pthread_barrier_t` object that's passed into this
// function isn't used. The last operation done to `*barrier` is to set
// `barrier->pwaitdata = NULL` to satisfy the requirement that this function
// leaves `*barrier` in a state as if `pthread_barrier_init()` had been called - and
// that operation is done by the thread that signals the wait condition
// completion before the completion is signaled.
// note: we're still holding `barrier->waitdata_mutex`;
rc = __mutex_lock( &pwaitdata->cond_mutex);
pwaitdata->waiter_count += 1;
if (pwaitdata->waiter_count < target_count) {
// need to wait for other threads
__mutex_unlock( &barrier->waitdata_mutex);
do {
// TODO: handle the return code from `__cond_wait()` to break out of this
// if a signal makes that necessary
__cond_wait( &pwaitdata->cond, &pwaitdata->cond_mutex);
} while (!pwaitdata->wait_complete);
}
else {
// this thread satisfies the wait - unblock all the other waiters
pwaitdata->wait_complete = 1;
// 'release' our use of the passed in pthread_barrier_t object
barrier->pwaitdata = NULL;
// unlock the barrier's waitdata_mutex - the barrier is
// ready for use by another set of threads
__mutex_unlock( barrier->waitdata_mutex);
// finally, unblock the waiting threads
__cond_broadcast( &pwaitdata->cond);
}
// at this point, barrier->waitdata_mutex is unlocked, the
// barrier->pwaitdata pointer has been cleared, and no further
// use of `*barrier` is permitted...
// however, each thread still has a valid `pwaitdata` pointer - the
// thread that owns that block needs to wait until all others have
// dropped the pwaitdata->waiter_count
// also, at this point the `pwaitdata->cond_mutex` is locked, so
// we're in a critical section
rc = 0;
pwaitdata->waiter_count--;
if (pwaitdata == &waitdata) {
// this thread owns the waitdata block - it needs to hang around until
// all other threads are done
// as a convenience, this thread will be the one that returns
// PTHREAD_BARRIER_SERIAL_THREAD
rc = PTHREAD_BARRIER_SERIAL_THREAD;
while (pwaitdata->waiter_count!= 0) {
__cond_wait( &pwaitdata->cond, &pwaitdata->cond_mutex);
};
__mutex_unlock( &pwaitdata->cond_mutex);
__cond_destroy( &pwaitdata->cond);
__mutex_destroy( &pwaitdata_cond_mutex);
}
else if (pwaitdata->waiter_count == 0) {
__cond_signal( &pwaitdata->cond);
__mutex_unlock( &pwaitdata->cond_mutex);
}
return rc;
}
2011年7月17日:更新 プロセス共有の障壁に関するコメント/質問への応答
プロセス間で共有される障壁の状況を完全に忘れました。そして、あなたが言及したように、私が概説したアイデアは、その場合に恐ろしく失敗するでしょう。私はPOSIX共有メモリの使用の経験が本当にないので、私がした提案はすべて 懐疑的に和らげるべきです.
要約するために(私の利益のために、他の誰もいない場合):
スレッドのいずれかが後に制御されるとき pthread_barrier_wait()
返品、バリアオブジェクトは「init」状態にある必要があります(ただし、最新の状態 pthread_barrier_init()
そのオブジェクトに設定)。また、APIで暗示されているのは、スレッドのいずれかが戻ってから、次のことの1つ以上が発生する可能性があることです。
- 別の呼びかけ
pthread_barrier_wait()
スレッドの同期の新しいラウンドを開始します pthread_barrier_destroy()
バリアオブジェクト上- バリアオブジェクトに割り当てられたメモリは、共有メモリ領域にある場合、解放されたり、非共有したりすることができます。
これらのことは、それ以前にそれを意味します pthread_barrier_wait()
呼び出しが許可されます どれか スレッドを返すには、すべての待機スレッドがその呼び出しのコンテキストでバリアオブジェクトを使用しなくなるようにする必要があります。私の最初の答えは、すべてのスレッドをブロックするバリアオブジェクトの外側にある同期オブジェクト(ミューテックスと関連する条件変数)の「ローカル」セットを作成することにより、これに対処しました。これらのローカル同期オブジェクトは、たまたま呼び出したスレッドのスタックに割り当てられました pthread_barrier_wait()
最初。
プロセス共有の障壁のために、似たようなことをする必要があると思います。ただし、その場合、スレッドのスタックにこれらの同期オブジェクトを単純に割り当てるだけではありません(他のプロセスにはアクセスできないため)。プロセス共有の障壁の場合、これらのオブジェクトはプロセス共有メモリに割り当てる必要があります。上記の手法は同様に適用できると思います。
-
waitdata_mutex
ローカル同期変数(waitdataブロック)の「割り当て」を制御することは、バリア構造体にあるため、すでに共有メモリに含まれています。もちろん、障壁が設定されているときTHEAD_PROCESS_SHARED
, 、その属性もに適用する必要がありますwaitdata_mutex
- いつ
__barrier_waitdata_init()
ローカルMutex&条件変数を初期化するために呼び出されます。スタックベースを単に使用するのではなく、共有メモリにそれらのオブジェクトを割り当てる必要がありますwaitdata
変数。 - 「クリーンアップ」スレッドがミューテックスと条件変数を破壊すると
waitdata
ブロック、また、ブロックのプロセス共有メモリ割り当てをクリーンアップする必要があります。 - 共有メモリが使用される場合、共有メモリオブジェクトが各プロセスで少なくとも1回開かれ、各プロセスで正しい回数を閉じていることを確認するためのメカニズムが必要です(ただし、スレッドごとに完全に閉じていませんプロセスはそれを使用して終了します)。私はそれがどのように行われるかを正確に考えていませんでした...
これらの変更により、スキームがプロセス共有の障壁で動作することができると思います。上の最後の箇条書きは、把握するための重要なアイテムです。もう1つは、「ローカル」プロセスを保持する共有メモリオブジェクトの名前を作成する方法です waitdata
. 。その名前に必要な特定の属性があります:
- 名前のストレージが
struct pthread_barrier_t
すべてのプロセスがアクセスできるように構造。それは、名前の長さの既知の制限を意味します - あなたは、一連の呼び出しの各「インスタンス」に固有の名前を望むでしょう
pthread_barrier_wait()
すべてのスレッドが最初のラウンドの待機から抜け出す前に、2回目のラウンドが開始するのを待つことが可能かもしれないからです(したがって、プロセス共有メモリブロックがセットアップされましたwaitdata
まだ解放されていないかもしれません)。したがって、名前はおそらくプロセスID、スレッドID、バリアオブジェクトのアドレス、アトミックカウンターなどに基づいている必要があります。 - 名前を「推測可能」にすることにセキュリティの影響があるかどうかはわかりません。もしそうなら、いくつかのランダム化を追加する必要があります - いくらを知りません。また、上記のデータをランダムビットとともにハッシュする必要があるかもしれません。私が言ったように、これが重要かどうかは本当にわかりません。
他のヒント
私が見る限り、必要はありません pthread_barrier_destroy
即時の操作になる。まだ目覚め段階にあるすべてのスレッドが目覚められるまで待つことができます。
たとえば、アトミックカウンターを持つことができます awakening
最初は目が覚めたスレッドの数に設定されています。その後、それは以前の最後のアクションとして減少します pthread_barrier_wait
戻り値。 pthread_barrier_destroy
その後、そのカウンターが落ちるまで回転する可能性があります 0
.