12

この質問は以下に基づいています:

pthreadバリアを破壊するのはいつ安全ですか?

および最近のglibcバグレポート:

http://sourceware.org/bugzilla/show_bug.cgi?id=12674

pthread_barrier_waitglibcで報告されたセマフォの問題についてはよくわかりませんが、上記のリンクされた質問のように、戻ったらすぐにバリアを破棄することが有効であると思われます。(通常、取得したスレッドPTHREAD_BARRIER_SERIAL_THREAD、またはバリアオブジェクトに対してすでに「責任がある」と見なされている「特別な」スレッドは、それを破棄するスレッドになります。)私が考えることができる主なユースケースは、バリアが次のように使用される場合です。新しいスレッドによるデータの使用を作成中のスレッドのスタックで同期し、新しいスレッドがデータを使用できるようになるまで作成中のスレッドが戻らないようにします。他のバリアは、おそらくプログラム全体のライフタイムと等しいか、他の同期オブジェクトによって制御されます。

いずれにせよ、実装はどのようにしてバリアの破壊(そしておそらくそれが存在するメモリのマッピング解除さえも)がpthread_barrier_waitスレッドに戻るとすぐに安全であることを保証できますか?まだ戻っていない他のスレッドは、作業を完了して戻るためにバリアオブジェクトの少なくとも一部を調べる必要があるようです。これは、上記のglibcバグレポートで、sem_post調整後にウェイター数を調べる必要があるのと同じです。セマフォ値。

4

2 に答える 2

8

pthread_barrier_wait()pthreads実装によって提供される可能性のある、ミューテックスおよび条件変数機能を使用する実装例を使用して、これに別の亀裂を入れます。この例では、パフォーマンスの考慮事項を処理しようとしないことに注意してください(具体的には、待機中のスレッドのブロックが解除されると、待機を終了するときにすべて再シリアル化されます)。Linux Futexオブジェクトのようなものを使用すると、パフォーマンスの問題に役立つと思いますが、Futexはまだ私の経験からかなり外れています。

また、この例がシグナルまたはエラーを正しく処理しているとは思えません(シグナルの場合はそうだとしても)。しかし、読者の練習問題として、これらのことに対する適切なサポートを追加できると思います。

私の主な恐れは、例に競合状態またはデッドロックが発生する可能性があることです(ミューテックスの処理は私が望むよりも複雑です)。また、これはまだコンパイルされていない例であることに注意してください。擬似コードとして扱います。また、私の経験は主に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;
    
    rc = __mutex_init( &barrier->waitdata_mutex, NULL);
    if (!rc) return rc;

    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日1:プロセス共有バリアに関するコメント/質問に応じて更新

プロセス間で共有される障壁の状況を完全に忘れました。そしてあなたが言うように、私が概説したアイデアはその場合恐ろしく失敗するでしょう。私は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()が呼び出されると、単にスタックベースのwaitdata変数を使用するのではなく、共有メモリにそれらのオブジェクトを割り当てる必要があります。
  • 'cleanup'スレッドがブロック内のミューテックスと条件変数を破棄する場合、waitdataブロックのプロセス共有メモリ割り当てもクリーンアップする必要があります。
  • 共有メモリが使用される場合、共有メモリオブジェクトが各プロセスで少なくとも1回開かれ、各プロセスで正しい回数閉じられるようにするための何らかのメカニズムが必要です(ただし、すべてのスレッドの前に完全に閉じられるわけではありません)プロセスはそれを使用して終了します)。私はそれがどのように行われるのか正確には考えていません...

これらの変更により、スキームがプロセス共有バリアで動作できるようになると思います。上記の最後の箇条書きは、理解するための重要な項目です。もう1つは、「ローカル」プロセス共有を保持する共有メモリオブジェクトの名前を作成する方法waitdataです。その名前に必要な特定の属性があります。

  • 名前のストレージをstruct pthread_barrier_t構造内に配置して、すべてのプロセスがその名前にアクセスできるようにする必要があります。これは、名前の長さに対する既知の制限を意味します
  • pthread_barrier_wait()すべてのスレッドが最初のラウンドの待機から完全に終了する前に、2番目のラウンドの待機が開始される可能性があるため、一連の呼び出しの各「インスタンス」に固有の名前を付ける必要があります(したがって、プロセス-に設定された共有メモリブロックはwaitdataまだ解放されていない可能性があります)。したがって、名前はおそらく、プロセスID、スレッドID、バリアオブジェクトのアドレス、アトミックカウンターなどに基づいている必要があります。
  • 名前を「推測可能」にすることでセキュリティに影響があるかどうかはわかりません。もしそうなら、いくつかのランダム化を追加する必要があります-どれだけかはわかりません。たぶん、ランダムビットと一緒に上記のデータをハッシュする必要があるでしょう。私が言ったように、これが重要かどうかは本当にわかりません。
于 2011-05-05T18:47:59.030 に答える
1

私が見る限りpthread_barrier_destroy、すぐに操作する必要はありません。まだウェイクアップフェーズにあるすべてのスレッドがウェイクアップされるまで待機させることができます。

awakeningたとえば、最初にウェイクアップされるスレッドの数に設定されるアトミックカウンターを持つことができます。次に、戻る前の最後のアクションとしてデクリメントされpthread_barrier_waitます。pthread_barrier_destroy次に、そのカウンターがに落ちるまで回転している可能性があります0

于 2011-05-04T16:55:47.080 に答える