2017-02-18 3 views
0

私は個人的な趣味時代のゲームエンジンで作業しており、マルチスレッドバッチエグゼキュータで作業しています。私はもともと、マスタとスレーブのスレッド間の通信を容易にするために、ロックレスキューとstd :: functionを並行して使用していましたが、メモリを厳密に制御できる軽量な方法割り当て:関数ポインタとメモリプール。マルチスレッドC++:キャッシュからバイパスキャッシュ

とにかく、私が問題に遭遇しました:他の人がNULLポインタを読み、これアサートに失敗しながら

関数ポインタは、関係なく、私がしようとするもの、1つのスレッドだけ正しく読み取れなかっなっています。

これはキャッシュに問題があると確信しています。私はすべてのスレッドが同じアドレスを持つことを確認しました。私はvolatile、intptr_t、std :: atomicと宣言しようとしましたが、あらゆる種類のcasting-fuを試しました。スレッドはすべて無視してキャッシュされたコピーを読み続けるようです。

私は、並行性が良好であることを確認するためにモデル検査では、マスタとスレーブをモデル化してきたが、何のライブロックやデッドロックが存在しない(共有変数すべてが正しく同期することを提供する)

void Executor::operator() (int me) { 
    while (true) { 
     printf("Slave %d waiting.\n", me); 
     { 
      std::unique_lock<std::mutex> lock(batch.ready_m); 
      while(!batch.running) batch.ready.wait(lock); 
      running_threads++; 
     } 
     printf("Slave %d running.\n", me); 
     BatchFunc func = batch.func; 
     assert(func != nullptr); 

     int index; 
     if (batch.store_values) { 
      while ((index = batch.item.fetch_add(1)) < batch.n_items) { 
       void* data = reinterpret_cast<void*>(batch.data_buffer + index * batch.item_size); 
       func(batch.share_data, data); 
      } 
     } 
     else { 
      while ((index = batch.item.fetch_add(1)) < batch.n_items) { 
       void** data = reinterpret_cast<void**>(batch.data_buffer + index * batch.item_size); 
       func(batch.share_data, *data); 
      } 
     } 

     // at least one thread finished, so make sure we won't loop back around 
     batch.running = false; 

     if (running_threads.fetch_sub(1) == 1) { // I am the last one 
      batch.done = true; // therefore all threads are done 
      batch.complete.notify_all(); 
     } 
    } 
} 

void Executor::run_batch() { 
    assert(!batch.running); 
    if (batch.func == nullptr || batch.n_items == 0) return; 

    batch.item.store(0); 

    batch.running = true; 
    batch.done = false; 
    batch.ready.notify_all(); 

    printf("Master waiting.\n"); 
    { 
     std::unique_lock<std::mutex> lock(batch.complete_m); 
     while (!batch.done) batch.complete.wait(lock); 
    } 
    printf("Master ready.\n"); 

    batch.func = nullptr; 
    batch.n_items = 0; 
} 

batch.funcですそれは

typedef void(*BatchFunc)(const void*, void*); 
struct JobBatch { 
    volatile BatchFunc func; 
    void* const share_data = operator new(SHARED_DATA_MAXSIZE); 

    intptr_t const data_buffer = reinterpret_cast<intptr_t>(operator new (EXEC_DATA_BUFFER_SIZE)); 
    volatile size_t item_size; 
    std::atomic<int> item; // Index into the data array 
    volatile int n_items = 0; 

    std::condition_variable complete; // slave -> master signal 
    std::condition_variable ready; // master -> slave signal 
    std::mutex complete_m; 
    std::mutex ready_m; 

    bool store_values = false; 

    volatile bool running = false; // there is work to do in the batch 
    volatile bool done = false; // there is no work left to do 

    JobBatch(); 
} batch; 
を扱っていますことを、別の関数で

template<typename SharedT, typename ItemT> 
void set_batch_job(void(*func)(const SharedT*, ItemT*), const SharedT& share_data, bool byValue = true) { 
    static_assert(sizeof(SharedT) <= SHARED_DATA_MAXSIZE, "Shared data too large"); 
    static_assert(std::is_pod<SharedT>::value, "Shared data type must be POD"); 
    assert(std::is_pod<ItemT>::value || !byValue); 
    assert(!batch.running); 
    batch.func = reinterpret_cast<volatile BatchFunc>(func); 
    memcpy(batch.share_data, (void*) &share_data, sizeof(SharedT)); 
    batch.store_values = byValue; 
    if (byValue) { 
     batch.item_size = sizeof(ItemT); 
    } 
    else { // store pointers instead of values 
     batch.item_size = sizeof(ItemT*); 
    } 
    batch.n_items = 0; 
} 

を設定し、ここでは構造体(とのtypedef)でされています

batch.funcへのすべての必要な読み取りと書き込みがスレッド間で正しく同期することを確認するにはどうすればよいですか?

私はVisual Studioを使用しており、x64 Debug Windows実行ファイルをコンパイルしています。 Intel i5、Windows 10、8GB RAM

+0

なぜ構造物に揮発性のものが複数あるのですか? – tambre

+0

[MVCE](http:// stackoverflow)を提供することをお勧めします。com/help/mcve)。 – tambre

+0

volatileの過剰さは、関数ポインタ以外の変数が、私が期待したように動作しないものに対する過度の処理でキャッシュ競合を引き起こしていることを心配しています。 – Beefster

答えて

0

私はC++メモリモデルを少し読んで、atomic_thread_fenceを使って解決策をまとめました。私は狂っているので、すべてがおそらく非常に壊れていて、ここで自分のシステムを動かすべきではありませんが、ちょっと、学ぶのは楽しいです!

あなたは他のスレッドが見たいものを書いて行われている時はいつでも基本的には、あなたが共有データを読み取る前にatomic_thread_fence(std::memory_order_acquire)を呼び出し、受信スレッド(S)でatomic_thread_fence(std::memory_order_release)

を呼び出す必要があります。

私の場合、リリースは条件変数を待つ直前に実行され、取得は他のスレッドが書き込んだデータを使用する直前に行う必要があります。

これにより、あるスレッドの書き込みが他のスレッドから確実に見えるようになります。

私は専門家ではないため、これはおそらく問題に取り組むための正しい方法ではなく、特定の運命に直面する可能性があります。例えば、私はまだデッドロック/ライブロックの問題があります。

tl; dr:正確にキャッシュのことではありません。アトミックメモリフェンスを使用しない限り、スレッドはデータを完全に同期させることはできません。

+0

ミューテックス、セマフォ、その他のロックは、メモリフェンシングを行います。そのようなロックが正しく使用された場合、追加のメモリフェンスは必要ありません。 – bazza

関連する問題