ラッチとバリア

ラッチとバリアはスレッド同期メカニズムにとって単純であり、一部のスレッドがカウンターがゼロになるまで待機できるようにします。おそらく C++20 では、std::latch、std::barrier、および std::flex_barrier の 3 つのバリエーションでラッチとバリアを取得します。

最初に、2 つの質問があります:

<オール>
  • スレッドを同期するこれら 3 つのメカニズムの違いは何ですか? std::latch は 1 回しか使用できませんが、std::barrier と std::flex_barrier は複数回使用できます。さらに、std::flex_barrier を使用すると、カウンターがゼロになったときに関数を実行できます。
  • C++11 および C++14 ではフューチャ、スレッド、または条件変数をロックと組み合わせて使用​​できない、ラッチとバリアがサポートするユースケースはどれですか?ラッチとバリアは新しいユースケースを提供しませんが、はるかに使いやすくなっています。また、内部でロックフリー メカニズムを使用することが多いため、パフォーマンスも向上します。
  • ここで、3 つの調整メカニズムを詳しく見ていきます。

    std::latch

    std::latch は、カウントダウンするカウンターです。その値はコンストラクターで設定されます。スレッドは thread.count_down_and_wait メソッドを使用してカウンターを減らすことができます カウンターがゼロになるまで待ちます。さらに、thread.count_down メソッド 待機せずにカウンターを 1 だけ減らします。 std::latch にはさらにメソッド thread.is_ready があります カウンターがゼロで、メソッド thread.wait があるかどうかをテストするため カウンターがゼロになるまで待ちます。 std::latch のカウンターをインクリメントまたはリセットする可能性がないため、再利用できません。

    std::latch の詳細については、cppreference.com のドキュメントを参照してください。

    以下は、提案 n4204 の短いコード スニペットです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    void DoWork(threadpool* pool) {
     latch completion_latch(NTASKS);
     for (int i = 0; i < NTASKS; ++i) {
     pool->add_task([&] {
     // perform work
     ...
     completion_latch.count_down();
     }));
     }
     // Block until work is done
     completion_latch.wait();
     }
    

    コンストラクターの std::latch completion_latch を NTASKS に設定します (2 行目)。スレッド プールは NTASKS を実行します (4 ~ 8 行目)。各タスクの最後 (7 行目) で、カウンターが減少します。 11 行目は、関数 DoWork を実行するスレッドのバリアであり、したがって小さなワークフローのバリアです。このスレッドは、すべてのタスクが完了するまで待機する必要があります。

    提案は vector を使用し、動的に割り当てられたスレッドを vectorworkers.push_back(new thread([&] {. これはメモリ リークです。 代わりに、スレッドを std::unique_ptr に入れるか、ベクトルで直接作成する必要があります:workers.emplace_back[&]{ 。この観察は、std::barrier および std::flex_barrier の例にも当てはまります。

    std::バリア

    std::barrier は std::latch によく似ています。微妙な違いは、カウンターが以前の値にリセットされるため、 std::barrier を複数回使用できることです。カウンターがゼロになるとすぐに、いわゆる完了フェーズが開始されます。この完了フェーズは、std::barrier が空の場合です。それは std::flex_barrier で変わります。 std::barrier には、std::arrive_and_wait と std::arrive_and_drop という 2 つの興味深いメソッドがあります。 std::arrive_and_wait の間 std::arrive_and_drop 同期ポイントで待機中 同期メカニズムから自身を削除します。

    std::flex_barrier と完了フェーズを詳しく見ていく前に、std::barrier の簡単な例を示します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void DoWork() {
     Tasks& tasks;
     int n_threads;
     vector<thread*> workers;
    
     barrier task_barrier(n_threads);
    
     for (int i = 0; i < n_threads; ++i) {
     workers.push_back(new thread([&] {
     bool active = true;
     while(active) {
     Task task = tasks.get();
     // perform task
     ...
     task_barrier.arrive_and_wait();
     }
     });
     }
     // Read each stage of the task until all stages are complete.
     while (!finished()) {
     GetNextStage(tasks);
     }
     }
    

    6 行目の std::barrier バリアは、タスクを数回実行する多数のスレッドを調整するために使用されます。スレッド数は n_threads です (3 行目)。各スレッドは task.get() を介してそのタスクを受け取り (12 行目)、それを実行し、そのタスクで完了するまで (15 行目)、すべてのスレッドがタスクを完了するまで待機します。その後、12 行目で active が true を返す限り、12 行目で新しいタスクを実行します。

    std::flex_barrier

    私の見解では、例にある std::flex_barrier の名前は少しわかりにくいです。たとえば、std::flex_barrier は notifying_barrier と呼ばれます。したがって、std::flex_barrier という名前を使用しました。

    std::flex_barrier には、std::barrier とは対照的に追加のコンストラクターがあります。このコンストラクターは、完了フェーズで呼び出される呼び出し可能ユニットによってパラメーター化できます。 callable ユニットは数値を返さなければなりません。この数値は、完了フェーズのカウンターの値を設定します。 -1 という数値は、カウンターが次の反復で同じ値を保持することを意味します。 -1 より小さい数値は使用できません。

    完了フェーズでは何が起こっていますか?

    <オール>
  • すべてのスレッドがブロックされています。
  • スレッドのブロックが解除され、呼び出し可能ユニットが実行されます。
  • 完了フェーズが完了すると、すべてのスレッドのブロックが解除されます。
  • コード スニペットは、std::flex_barrier の使用法を示しています。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
     void DoWork() {
     Tasks& tasks;
     int initial_threads;
     atomic<int> current_threads(initial_threads);
     vector<thread*> workers;
    
     // Create a flex_barrier, and set a lambda that will be
     // invoked every time the barrier counts down. If one or more
     // active threads have completed, reduce the number of threads.
     std::function rf = [&] { return current_threads;};
     flex_barrier task_barrier(n_threads, rf);
    
     for (int i = 0; i < n_threads; ++i) {
     workers.push_back(new thread([&] {
     bool active = true;
     while(active) {
     Task task = tasks.get();
     // perform task
     ...
     if (finished(task)) {
     current_threads--;
     active = false;
     }
     task_barrier.arrive_and_wait();
     }
     });
     }
    
     // Read each stage of the task until all stages are complete.
     while (!finished()) {
     GetNextStage(tasks);
     }
     }
    

    この例は、std::barrier の例と同様の戦略に従います。違いは、今回は std::flex_barrier のカウンターが実行時に調整されることです。したがって、11 行目の std::flex_barrier task_barrier はラムダ関数を取得します。このラムダ関数は、その変数 current_thread を参照によってキャプチャします。変数は 21 行目でデクリメントされ、スレッドがタスクを完了した場合は active が false に設定されます。したがって、カウンターは完了フェーズで減少します。

    std::flex_barrier には、std::barrier および std::latch とは対照的に 1 つの特殊性があります。カウンターを増やすことができるのはこれだけです。

    詳細については、cppreference.com で std::latch、std::barrier、および std::flex_barrier を参照してください。

    次は?

    コルーチンは、状態を維持しながら一時停止および再開できる一般化された関数です。これらは、オペレーティング システムでの協調タスク、イベント システムでのイベント ループ、無限リスト、またはパイプラインの実装によく使用されます。次の投稿でコルーチンの詳細を読むことができます。