web-dev-qa-db-ja.com

C ++ 11のロックフリーの複数の生産者/消費者キュー

C++ 11で、ロックフリーの複数のプロデューサー、複数のコンシューマーキューを実装しようとしています。私はこれを学習演習として行っているので、既存のオープンソース実装を使用できることはよく知っていますが、コードが機能しない理由を知りたいと思います。データはリングバッファに保存され、明らかに「制限付きMPMCキュー」です。

Disruptorについて読んだものにかなり近いモデルを作成しました。私が気付いたのは、それが単一の消費者と単一/複数のプロデューサーで完全にうまく機能することです。

キューは次のとおりです。

    template <typename T>
class Queue : public IQueue<T>
{
public:
    explicit Queue( int capacity );
    ~Queue();

    bool try_Push( T value );
    bool try_pop( T& value );
private:
    typedef struct
    {
        bool readable;
        T value;
    } Item;

    std::atomic<int> m_head;
    std::atomic<int> m_tail;
    int m_capacity;
    Item* m_items;
};

template <typename T>
Queue<T>::Queue( int capacity ) :
m_head( 0 ),
m_tail( 0 ),
m_capacity(capacity),
m_items( new Item[capacity] )
{
    for( int i = 0; i < capacity; ++i )
    {
        m_items[i].readable = false;
    }
}

template <typename T>
Queue<T>::~Queue()
{
    delete[] m_items;
}

template <typename T>
bool Queue<T>::try_Push( T value )
{
    while( true )
    {
        // See that there's room
        int tail = m_tail.load(std::memory_order_acquire);
        int new_tail = ( tail + 1 );
        int head = m_head.load(std::memory_order_acquire);

        if( ( new_tail - head ) >= m_capacity )
        {
            return false;
        }

        if( m_tail.compare_exchange_weak( tail, new_tail, std::memory_order_acq_rel ) )
        {
            // In try_pop, m_head is incremented before the reading of the value has completed,
            // so though we've acquired this slot, a consumer thread may be in the middle of reading
            tail %= m_capacity;

            std::atomic_thread_fence( std::memory_order_acquire );
            while( m_items[tail].readable )
            {
            }

            m_items[tail].value = value;
            std::atomic_thread_fence( std::memory_order_release );
            m_items[tail].readable = true;

            return true;
        }
    }
}

template <typename T>
bool Queue<T>::try_pop( T& value )
{
    while( true )
    {
        int head = m_head.load(std::memory_order_acquire);
        int tail = m_tail.load(std::memory_order_acquire);

        if( head == tail )
        {
            return false;
        }

        int new_head = ( head + 1 );

        if( m_head.compare_exchange_weak( head, new_head, std::memory_order_acq_rel ) )
        {
            head %= m_capacity;

            std::atomic_thread_fence( std::memory_order_acquire );
            while( !m_items[head].readable )
            {
            }

            value = m_items[head].value;
            std::atomic_thread_fence( std::memory_order_release );
            m_items[head].readable = false;

            return true;
        }
    }
}

そして、これが私が使用しているテストです:

void Test( std::string name, Queue<int>& queue )
{
    const int NUM_PRODUCERS = 64;
    const int NUM_CONSUMERS = 2;
    const int NUM_ITERATIONS = 512;
    bool table[NUM_PRODUCERS*NUM_ITERATIONS];
    memset(table, 0, NUM_PRODUCERS*NUM_ITERATIONS*sizeof(bool));

    std::vector<std::thread> threads(NUM_PRODUCERS+NUM_CONSUMERS);

    std::chrono::system_clock::time_point start, end;
    start = std::chrono::system_clock::now();

    std::atomic<int> pop_count (NUM_PRODUCERS * NUM_ITERATIONS);
    std::atomic<int> Push_count (0);

    for( int thread_id = 0; thread_id < NUM_PRODUCERS; ++thread_id )
    {
        threads[thread_id] = std::thread([&queue,thread_id,&Push_count]()
                                 {
                                     int base = thread_id * NUM_ITERATIONS;

                                     for( int i = 0; i < NUM_ITERATIONS; ++i )
                                     {
                                         while( !queue.try_Push( base + i ) ){};
                                         Push_count.fetch_add(1);
                                     }
                                 });
    }

    for( int thread_id = 0; thread_id < ( NUM_CONSUMERS ); ++thread_id )
    {
        threads[thread_id+NUM_PRODUCERS] = std::thread([&]()
                                         {
                                             int v;

                                             while( pop_count.load() > 0 )
                                             {
                                                 if( queue.try_pop( v ) )
                                                 {
                                                     if( table[v] )
                                                     {
                                                         std::cout << v << " already set" << std::endl;
                                                     }
                                                     table[v] = true;
                                                     pop_count.fetch_sub(1);
                                                 }
                                             }
                                         });

    }

    for( int i = 0; i < ( NUM_PRODUCERS + NUM_CONSUMERS ); ++i )
    {
        threads[i].join();
    }

    end = std::chrono::system_clock::now();
    std::chrono::duration<double> duration = end - start;

    std::cout << name << " " << duration.count() << std::endl;

    std::atomic_thread_fence( std::memory_order_acq_rel );

    bool result = true;
    for( int i = 0; i < NUM_PRODUCERS * NUM_ITERATIONS; ++i )
    {
        if( !table[i] )
        {
            std::cout << "failed at " << i << std::endl;
            result = false;
        }
    }
    std::cout << name << " " << ( result? "success" : "fail" ) << std::endl;
}

正しい方向への微調整は大歓迎です。私はすべてにミューテックスを使用するのではなく、メモリフェンスにかなり慣れていないので、おそらく根本的に何かを誤解しているだけです。

乾杯J

9
Joe

Moody Camel の実装を見てみましょう。

これは、完全にC++ 11で記述されたC++用の高速な汎用ロックフリーキューです。ドキュメントは、いくつかのパフォーマンステストとともにかなり良いようです。

他のすべての興味深いもの(とにかく読む価値があります)の中で、それはすべて単一のヘッダーに含まれており、簡略化されたBSDライセンスの下で利用できます。プロジェクトにドロップしてお楽しみください!

10
EnzoR

最も単純なアプローチは、循環バッファを使用します。つまり、256要素の配列のようなもので、uint8_tをインデックスとして使用するため、オーバーフローするとラップアラウンドして最初から開始します。

構築できる最も単純なプリミティブは、単一のプロデューサー、単一のコンシューマースレッドがある場合です。

バッファには2つのヘッドがあります。

  • 書き込みヘッド:次に書き込まれる要素を指します。
  • 読み取りヘッド:次に読み取られる要素を指します。

プロデューサーの操作:

  1. 書き込みヘッド+1 ==読み取りヘッドの場合、バッファがいっぱいです。バッファフルエラーを返します。
  2. 要素にコンテンツを書き込みます。
  3. CPUコアを同期するためにメモリバリアを挿入します。
  4. 書き込みヘッドを前方に移動します。

バッファがいっぱいの場合でも、まだ1つの部屋が残っていますが、バッファが空の場合と区別するために、それを予約します。

消費者の操作:

  1. 読み取りヘッド==書き込みヘッドの場合、バッファは空です。バッファ空エラーを返します。
  2. 要素の内容を読み取ります。
  3. CPUコアを同期するためにメモリバリアを挿入します。
  4. 読み取りヘッドを前方に移動します。

プロデューサーは書き込みヘッドを所有し、コンシューマーは読み取りヘッドを所有します。これらに同時実行性はありません。また、操作が完了するとヘッドが更新されます。これにより、コンシューマーは完成した要素を残し、コンシューマーは完全に消費された空のセルを残します。

新しいスレッドをフォークするたびに、これらのパイプを両方向に2つ作成し、スレッドと双方向通信できるようにします。

ロックの解放について話していることを考えると、どのスレッドもブロックされていないことも意味します。スレッドが空に回転していることを何もすることがない場合は、これを検出して、発生時にスリープを追加することをお勧めします。

2
Calmarius

これはどうですか ロックフリーキュー

これはメモリオーダリングのロックフリーキューですが、キューを初期化するときに現在のスレッドの数を事前に設定する必要があります。

例えば:-

int* ret;
int max_concurrent_thread = 16;
lfqueue_t my_queue;

lfqueue_init(&my_queue, max_concurrent_thread );

/** Wrap This scope in other threads **/
int_data = (int*) malloc(sizeof(int));
assert(int_data != NULL);
*int_data = i++;
/*Enqueue*/
 while (lfqueue_enq(&my_queue, int_data) == -1) {
    printf("ENQ Full ?\n");
}

/** Wrap This scope in other threads **/
/*Dequeue*/
while  ( (int_data = lfqueue_deq(&my_queue)) == NULL) {
    printf("DEQ EMPTY ..\n");
}

// printf("%d\n", *(int*) ret );
free(ret);
/** End **/

lfqueue_destroy(&my_queue);
0
Oktaheta