web-dev-qa-db-ja.com

円形ロックフリーバッファー

現在、データフィードの1つ以上のストリームに接続し、結果に基づいてイベントをトリガーするよりもデータを分析するシステムを設計しています。典型的なマルチスレッドのプロデューサー/コンシューマーのセットアップでは、データをキューに入れる複数のプロデューサースレッドと、データを読み取る複数のコンシューマースレッドがあり、コンシューマーは最新のデータポイントとn個のポイントのみに関心があります。プロデューサースレッドは、低速なコンシューマーが処理できない場合はブロックする必要があります。もちろん、未処理の更新がない場合、コンシューマースレッドはブロックします。リーダー/ライターロックを備えた一般的な同時キューを使用するとうまく機能しますが、入ってくるデータのレートは非常に大きくなる可能性があるため、プロデューサーのロックオーバーヘッド、特にライターロックを削減したいと考えました。循環ロックフリーバッファが必要だと思います。

次の2つの質問:

  1. 循環ロックフリーバッファーが答えですか?

  2. もしそうなら、私が自分自身をロールバックする前に、私のニーズに合うパブリック実装を知っていますか?

循環ロックフリーバッファを実装する際のポインタはいつでも歓迎します。

ところで、これはLinuxのC++で行います。

追加情報:

応答時間はシステムにとって重要です。理想的には、1ミリ秒の余分な遅延がシステムを無価値にしたり、価値を大幅に低下させたりする可能性があるため、コンシューマスレッドはできるだけ早く更新を確認したいと思うでしょう。

私が傾いているデザインのアイデアは、セミロックフリーの循環バッファで、プロデューサスレッドはできるだけ早くバッファにデータを入れ、バッファがいっぱいでない限りブロックせずにバッファAのヘッドを呼び出しましょうAはバッファZの終わりを満たします。コンシューマスレッドはそれぞれ、循環バッファPとPへの2つのポインタを保持しますn、ここでPはスレッドのローカルバッファーヘッド、Pn Pの後のn番目のアイテムです。各コンシューマスレッドはPとPを進めますn 現在のPの処理が終了し、バッファポインタZの終わりが最も遅いPで進められるn。 PがAに追いつくと、これは処理する新しい更新がなくなることを意味し、消費者はスピンし、Aが再び進むのを待ちます。コンシューマースレッドが長すぎる場合、スリープ状態にして条件変数を待つことができますが、レイテンシーが増加しないため、CPUサイクルが更新を待機しているコンシューマーで問題ありません(CPUコアが増えます)スレッドよりも)。循環トラックがあり、プロデューサーが多数の消費者の前で実行されていることを想像してください。キーは、プロデューサーが通常消費者のほんの数歩先に実行されるようにシステムを調整することです。ロックフリー技術を使用して行われます。実装の詳細を正しく理解することは簡単ではないことを理解しています...大丈夫、非常に難しいので、自分のいくつかを作る前に他人の間違いから学びたいのです。

68
Shing Yip

ここ数年、ロックフリーのデータ構造に関する特定の研究を行ってきました。私はこの分野のほとんどの論文を読みました(実際に使用されているのは約10か15だけですが、約40ほどしかありません:-)

知る限り、ロックフリーの循環バッファは発明されていません。問題は、読者が作家を追い越す、またはその逆の複雑な状態に対処することです。

ロックフリーのデータ構造を少なくとも6か月間勉強していない場合は、自分でデータ構造を書こうとしないでください。あなたはそれを間違えますし、新しいプラットフォームにコードがデプロイされてから失敗するまで、エラーが存在することはあなたには明らかではないかもしれません。

しかし、あなたの要件には解決策があると思います。

ロックフリーキューとロックフリーフリーリストをペアにする必要があります。

空きリストは事前割り当てを提供するため、ロックフリーアロケーターの(財政的に高価な)要件が不要になります。空きリストが空の場合、キューから要素を即座にデキューし、代わりにそれを使用することにより、循環バッファの動作を複製します。

(もちろん、ロックベースの循環バッファーでは、ロックが取得されると、要素の取得は非常に高速です-基本的には単にポインターの逆参照です-しかし、ロックフリーのアルゴリズムではそれを取得できません。フリーリストポップに続いてデキューに失敗するオーバーヘッドは、ロックフリーアルゴリズムが実行する必要がある作業量と同程度です)。

マイケルとスコットは、1996年に非常に優れたロックフリーキューを開発しました。以下のリンクから、論文のPDFを追跡するのに十分な詳細が得られます。 マイケルとスコット、FIFO

ロックフリーのフリーリストは最も単純なロックフリーのアルゴリズムであり、実際に私はそれについて実際の論文を見たことがないと思います。

36
user82238

あなたが望むものの専門用語は、ロックフリーキューです。 コードと論文へのリンクを含む優れたメモのセット Ross Bencinaがあります。私が最も信頼している作品は Maurice Herlihy です(アメリカ人の場合、彼は「モリス」のようなファーストネームを発音します)。

33
Norman Ramsey

バッファーが空またはいっぱいの場合にプロデューサーまたはコンシューマーがブロックするという要件は、データが利用可能になるまでプロデューサーとコンシューマーをブロックするためにセマフォまたは条件変数を使用して、通常のロックデータ構造を使用する必要があることを示唆しています。ロックフリーコードは通常、このような状況ではブロックしません。OSを使用してブロックする代わりに、実行できない操作をスピンまたは破棄します。 (別のスレッドがデータを生成または消費するまで待つ余裕がある場合、なぜ別のスレッドがデータ構造の更新を完了するのを待つのですか?)

(x86/x64)Linuxでは、競合がない場合、ミューテックスを使用したスレッド内同期は比較的安価です。生産者と消費者がロックを保持する必要がある時間を最小限に抑えることに集中します。記録された最後のN個のデータポイントのみに関心があると言ったので、循環バッファーはこれをかなりうまくやると思います。ただし、ブロック要件と、消費者が実際に読み取ったデータを消費(削除)するという考え方と、これがどのように適合するのか、私にはよくわかりません。 (消費者に最後のN個のデータポイントのみを見て、削除しないようにしたいですか?消費者が追いつかないかどうかをプロデューサーに気にせず、古いデータを上書きしたいだけですか?)

また、Zan Lynxがコメントしたように、大量のデータを受信した場合、データをより大きなチャンクに集約/バッファリングできます。一定数のポイント、または一定時間内に受信したすべてのデータをバッファリングできます。これは、同期操作が少なくなることを意味します。ただし、遅延は発生しますが、リアルタイムLinuxを使用していない場合は、とにかくある程度対処する必要があります。

11
Doug

Boostライブラリでの実装は検討する価値があります。使いやすく、かなり高いパフォーマンスです。テストを作成し、クアッドコアi7ラップトップ(8スレッド)で実行し、1秒あたり約400万のエンキュー/デキュー操作を取得しました。これまで言及されていない別の実装は、 http://moodycamel.com/blog/2014/detailed-design-of-a-lock-free-queue のMPMCキューです。 32の生産者と32の消費者がいる同じラップトップで、この実装を使用して簡単なテストを行いました。宣伝されているように、ブーストロックレスキューよりも高速です。

他の答えのほとんどは、状態のロックレスプログラミングは難しいことです。ほとんどの実装では、修正に多くのテストとデバッグを必要とするコーナーケースを検出するのが困難です。これらは通常、コードにメモリバリアを慎重に配置することで修正されます。また、多くの学術記事で公開されている正当性の証明もあります。私はこれらの実装をブルートフォースツールでテストすることを好みます。実稼働環境で使用する予定のロックレスアルゴリズムは、 http://research.Microsoft.com/en-us/um/people/lamport/tla/tla.html のようなツールを使用して、正確性を確認する必要があります。 。

5
Alex

これについてかなり良いシリーズの記事があります DDJ 。このようなものがどれほど難しいのかを示す兆候として、 以前の記事 の修正が間違っていました。間違いを理解する前に、間違いを理解してください)-;

5
Henk Holterman

私はハードウェアメモリモデルとロックのないデータ構造の専門家ではありません。プロジェクトでそれらを使用することは避け、従来のロックされたデータ構造を使用します。

しかし、最近ビデオに気付きました: リングバッファに基づくロックレスSPSCキュー

これは、オープンソースの高性能Java取引システムで使用されるLMAX distruptorというライブラリ: LMAX Distruptor に基づいています。

上記のプレゼンテーションに基づいて、ヘッドポインターとテールポインターをアトミックにし、ヘッドがテールを​​後ろからキャッチした状態、またはその逆の状態をアトミックにチェックします。

以下に、非常に基本的なC++ 11実装を示します。

// USING SEQUENTIAL MEMORY
#include<thread>
#include<atomic>
#include <cinttypes>
using namespace std;

#define RING_BUFFER_SIZE 1024  // power of 2 for efficient %
class lockless_ring_buffer_spsc
{
    public :

        lockless_ring_buffer_spsc()
        {
            write.store(0);
            read.store(0);
        }

        bool try_Push(int64_t val)
        {
            const auto current_tail = write.load();
            const auto next_tail = increment(current_tail);
            if (next_tail != read.load())
            {
                buffer[current_tail] = val;
                write.store(next_tail);
                return true;
            }

            return false;  
        }

        void Push(int64_t val)
        {
            while( ! try_Push(val) );
            // TODO: exponential backoff / sleep
        }

        bool try_pop(int64_t* pval)
        {
            auto currentHead = read.load();

            if (currentHead == write.load())
            {
                return false;
            }

            *pval = buffer[currentHead];
            read.store(increment(currentHead));

            return true;
        }

        int64_t pop()
        {
            int64_t ret;
            while( ! try_pop(&ret) );
            // TODO: exponential backoff / sleep
            return ret;
        }

    private :
        std::atomic<int64_t> write;
        std::atomic<int64_t> read;
        static const int64_t size = RING_BUFFER_SIZE;
        int64_t buffer[RING_BUFFER_SIZE];

        int64_t increment(int n)
        {
            return (n + 1) % size;
        }
};

int main (int argc, char** argv)
{
    lockless_ring_buffer_spsc queue;

    std::thread write_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.Push(i);
             }
         }  // End of lambda expression
                                                );
    std::thread read_thread( [&] () {
             for(int i = 0; i<1000000; i++)
             {
                    queue.pop();
             }
         }  // End of lambda expression
                                                );
    write_thread.join();
    read_thread.join();

     return 0;
}
4
Akin Ocal

サッターのキューは次善であり、彼はそれを知っています。 Art of Multicoreプログラミングは素晴らしい参考資料ですが、メモリモデルのJava guys、period.Rossのリンクはそのような問題にライブラリがあるため明確な答えを得ることができません。に。

ロックフリープログラミングを行うことは、問題を解決する前に明らかに過剰なエンジニアリングに時間を費やしたい場合を除き、トラブルを求めています(その説明から判断すると、「完璧を求めている」という一般的な狂気です'キャッシュの一貫性)。それには何年もかかり、最初に問題を解決せず、後に一般的な病気である最適化につながります。

4
rama-jka toti

この記事 に同意し、ロックフリーのデータ構造の使用を推奨します。ロックフリーfifoキューに関する比較的最近の論文は this です。同じ著者によるさらなる論文を検索します。ロックフリーのデータ構造に関するChalmersの博士論文もあります(リンクを失いました)。ただし、要素の大きさは言わなかった-ロックフリーのデータ構造はWordサイズのアイテムでのみ効率的に機能するため、要素がマシンのWord(32または64よりも大きい場合)を動的に割り当てる必要があるビット)。要素を動的に割り当てる場合は、(プログラムのプロファイルを作成しておらず、基本的に時期尚早な最適化を行っているため)ボトルネックをメモリアロケータにシフトするため、ロックのないメモリアロケータが必要です。たとえば、 Streamflow 、およびアプリケーションと統合します。

4
zvrba

競合を減らすための便利なテクニックの1つは、アイテムを複数のキューにハッシュし、各コンシューマーを「トピック」専用にすることです。

消費者が関心を持っている最新のアイテムの数については、キュー全体をロックし、それを繰り返してオーバーライドするアイテムを見つけたくない-アイテムをNタプル、つまりすべてN最近のアイテム。プロデューサーがタイムアウトでフルキュー(コンシューマーが追いつかない場合)でブロックし、ローカルタプルキャッシュを更新する実装のボーナスポイント-データソースにバックプレッシャーをかけません。

4

完全を期すために、 OtlContainers には十分にテストされたロックフリー循環バッファーがありますが、Delphiで記述されています(TOmniBaseBoundedQueueは循環バッファーであり、TOmniBaseBoundedStackは境界スタックです)。同じユニット(TOmniBaseQueue)には無制限のキューもあります。制限のないキューについては、 動的ロックフリーキュー-正しく実行 で説明しています。境界キュー(循環バッファー)の初期実装は ロックフリーキュー、最後に! で説明されていましたが、その後コードが更新されました。

2
gabr

Disruptor使用方法 )をチェックしてください。これは、複数のスレッドがサブスクライブできるリングバッファです。

2
Rolf Kristensen

これは古い質問ですが、誰も言及していません [〜#〜] dpdk [〜#〜] のロックレスリングバッファー。複数のプロデューサーと複数のコンシューマーをサポートする高スループットのリングバッファーです。また、シングルコンシューマモードとシングルプロデューサモードを提供し、SPSCモードではリングバッファは待機しません。 Cで書かれており、複数のアーキテクチャをサポートしています。

さらに、アイテムをバルクでキューに入れる/デキューできるバルクモードとバーストモードをサポートしています。この設計では、アトミックポインターを移動してスペースを予約するだけで、複数のコンシューマーまたは複数のプロデューサーがキューに同時に書き込むことができます。

2
Saman Barghi

これは古いスレッドですが、まだ言及されていないため、ロックフリーで循環型の1つのプロデューサー-> 1つのコンシューマーがあります。FIFO JUCE C++フレームワークで利用可能です。

https://www.juce.com/doc/classAbstractFifo#details

2
Nikolay Tsenkov

ここに私がそれをする方法があります:

  • キューを配列にマッピングします
  • 次の読み取りインデックスと次の次の書き込みインデックスで状態を維持する
  • 空のフルビットベクトルを保持する

挿入は、CASを増分で使用し、次の書き込みでロールオーバーすることで構成されます。スロットができたら、値を追加してから、それに一致する空/フルビットを設定します。

削除では、アンダーフローでテストする前にビットをチェックする必要がありますが、それ以外は書き込みの場合と同じですが、読み取りインデックスを使用し、空/フルビットをクリアします。

注意してください、

  1. 私はこれらのことの専門家ではありません
  2. アトミックASM操作は私が使用したときに非常に遅いように見えるので、それらのいくつかを超える場合は、挿入/削除関数内に埋め込まれたロックを使用する方が速いかもしれません。理論的には、ロックを取得する単一のアトミックopに続いて(非常に)少数の非アトミックASM opsが、いくつかのアトミックopsで行われるものよりも高速である可能性があります。しかし、この作業を行うには、手動または自動のインライン化が必要になるため、ASMの1つの短いブロックになります。
1
BCS

特にプロデューサーとコンシューマーが1つしかない場合、競合状態を防ぐためにロックする必要がない状況があります。

LDD3からの次の段落を検討してください。

慎重に実装すると、複数のプロデューサーまたはコンシューマーが存在しない場合、循環バッファーはロックを必要としません。プロデューサーは、書き込みインデックスとそれが指す配列の場所を変更できる唯一のスレッドです。ライターが書き込みインデックスを更新する前に新しい値をバッファーに保存する限り、リーダーは常に一貫したビューを表示します。リーダーは、読み取りインデックスとそれが指す値にアクセスできる唯一のスレッドです。 2つのポインターが互いにオーバーランしないように注意することで、プロデューサーとコンシューマーは競合状態なしで同時にバッファーにアクセスできます。

0
Dražen G.

lfqueue を試すことができます

それは使いやすいです、それは自由な円形の設計ロックです

int *ret;

lfqueue_t results;

lfqueue_init(&results);

/** Wrap This scope in multithread testing **/
int_data = (int*) malloc(sizeof(int));
assert(int_data != NULL);
*int_data = i++;
/*Enqueue*/
while (lfqueue_enq(&results, int_data) != 1) ;

/*Dequeue*/
while ( (ret = lfqueue_deq(&results)) == NULL);

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

lfqueue_clear(&results);
0
Oktaheta