C++20 のバリアとアトミック スマート ポインター

前回の投稿で、C++20 のラッチを紹介しました。ラッチを使用すると、カウンターがゼロになるまでスレッドを待機させることができます。さらに、ラッチには、その大きな兄弟バリアを複数回使用できます。今日は、バリアについて書き、アトミック スマート ポインターを紹介します。

std::latch に慣れていない場合は、前回の投稿「Latches in C++20」をお読みください。

std::barrier

std::latch には 2 つの違いがあります そして std::barrier . std::latch 複数のスレッドで 1 つのタスクを管理するのに役立ちます。 std::barrier 複数のスレッドで繰り返されるタスクを管理するのに役立ちます。さらに、std::barrier いわゆる完了ステップで機能を実行できます。完了ステップは、カウンターがゼロになった状態です。カウンターがゼロになった直後に、いわゆる完了ステップが開始されます。この完了ステップでは、callable が呼び出されます。 std::barrier コンストラクターで呼び出し可能オブジェクトを取得します。呼び出し可能ユニット (略して callable) は、関数のように振る舞うものです。これらの名前付き関数だけでなく、関数オブジェクトまたはラムダ式も含まれます。

完了ステップでは、次のステップを実行します:

<オール>
  • すべてのスレッドがブロックされています。
  • 任意のスレッドのブロックが解除され、callable が実行されます。
  • 完了ステップが完了すると、すべてのスレッドのブロックが解除されます。
  • 次の表は、std::barrier bar. のインターフェースを示しています。

    call bar.arrive_and_drop() call は本質的に、カウンターが次のフェーズのために 1 つ減らされることを意味します。次のプログラム fullTimePartTimeWorkers.cpp 第 2 フェーズのワーカー数が半分になります。

    // fullTimePartTimeWorkers.cpp
    
    #include <iostream>
    #include <barrier>
    #include <mutex>
    #include <string>
    #include <thread>
    
    std::barrier workDone(6);
    std::mutex coutMutex;
    
    void synchronizedOut(const std::string& s) noexcept {
     std::lock_guard<std::mutex> lo(coutMutex);
     std::cout << s;
    }
    
    class FullTimeWorker { // (1)
     public:
     FullTimeWorker(std::string n): name(n) { };
     
     void operator() () {
     synchronizedOut(name + ": " + "Morning work done!\n");
     workDone.arrive_and_wait(); // Wait until morning work is done (3)
     synchronizedOut(name + ": " + "Afternoon work done!\n");
     workDone.arrive_and_wait(); // Wait until afternoon work is done (4)
     
     }
     private:
     std::string name;
    };
     
    class PartTimeWorker { // (2)
     public:
     PartTimeWorker(std::string n): name(n) { };
     
     void operator() () {
     synchronizedOut(name + ": " + "Morning work done!\n");
     workDone.arrive_and_drop(); // Wait until morning work is done // (5)
     }
     private:
     std::string name;
    };
    
    int main() {
    
     std::cout << '\n';
    
     FullTimeWorker herb(" Herb");
     std::thread herbWork(herb);
     
     FullTimeWorker scott(" Scott");
     std::thread scottWork(scott);
     
     FullTimeWorker bjarne(" Bjarne");
     std::thread bjarneWork(bjarne);
     
     PartTimeWorker andrei(" Andrei");
     std::thread andreiWork(andrei);
     
     PartTimeWorker andrew(" Andrew");
     std::thread andrewWork(andrew);
     
     PartTimeWorker david(" David");
     std::thread davidWork(david);
    
     herbWork.join();
     scottWork.join();
     bjarneWork.join();
     andreiWork.join();
     andrewWork.join();
     davidWork.join();
     
    }
    

    このワークフローは、フルタイムのワーカー (1) とパートタイムのワーカー (2) の 2 種類のワーカーで構成されます。アルバイトは午前中に働き、正社員は午前と午後に働きます。したがって、フルタイムの労働者は workDone.arrive_and_wait() を呼び出します。 (行 (3) および (4)) 2 回。逆にアルバイトは workDone.arrive_and_drop() に電話 (5) 一度だけ。この workDone.arrive_and_drop() 呼び出しにより、パートタイム労働者は午後の仕事をスキップします。したがって、カウンターは最初のフェーズ (朝) で値 6 を持ち、2 番目のフェーズ (午後) で値 3 を持ちます。

    さて、atomics への投稿を見逃していました。

    アトミック スマート ポインター

    std::shared_ptr 制御ブロックとそのリソースで構成されます。制御ブロックはスレッドセーフですが、リソースへのアクセスはそうではありません。つまり、参照カウンターの変更はアトミック操作であり、リソースが 1 回だけ削除されることが保証されます。これらは std::shared_ptr の保証です

    それどころか、std::shared_ptr が重要です。 には、明確に定義されたマルチスレッド セマンティクスがあります。一見、std::shared_ptr の使用 マルチスレッド コードの賢明な選択ではないようです。これは、定義上、共有および変更可能であり、非同期の読み取りおよび書き込み操作、したがって未定義の動作の理想的な候補です。一方、最新の C++ には次のガイドラインがあります。未加工のポインターを使用しない .したがって、共有所有権をモデル化する場合は、マルチスレッド プログラムでスマート ポインターを使用する必要があります。

    アトミック スマート ポインターの提案 N4162 は、現在の実装の欠陥に直接対処しています。欠点は、一貫性、正確性、パフォーマンスの 3 つの点に集約されます。

    • 一貫性 :アトミック操作 std::shared_ptr 非アトミック データ型に対する唯一のアトミック操作です。
    • 正しさ :グローバル アトミック操作の使用法は、正しい使用法が規律に基づいているため、非常にエラーが発生しやすくなります。 ptr = localPtr を使用するなど、アトミック操作を使用することを忘れがちです。 std::atomic_store(&ptr, localPt の代わりに r)。その結果、データ競合のために未定義の動作が発生します。代わりにアトミック スマート ポインターを使用した場合、型システムはそれを許可しません。
    • パフォーマンス :アトミック スマート ポインターは、無料の atomic_ に比べて大きな利点があります。 * 機能。アトミック バージョンは特別なユース ケース向けに設計されており、内部的に std::atomic_flag を持つことができます。 安価なスピンロックの一種として。ポインター関数の非アトミック バージョンをスレッド セーフになるように設計することは、それらがシングル スレッドのシナリオで使用される場合、やり過ぎです。パフォーマンスが低下します。

    正しさの議論はおそらく最も重要なものです。なんで?その答えは提案にあります。この提案は、要素の挿入、削除、および検索をサポートする、スレッドセーフな単一リンク リストを提示します。この単一リンク リストは、ロックのない方法で実装されています。

    C++11 コンパイラでプログラムをコンパイルするために必要なすべての変更は、赤でマークされています。アトミック スマート ポインターを使用した実装ははるかに簡単であるため、エラーが発生しにくくなります。 C++20 の型システムでは、アトミック スマート ポインターで非アトミック操作を使用することは許可されていません。

    提案 N4162 は新しい型 std::atomic_shared_ptr を提案しました と std::atomic_weak_ptr アトミックスマートポインターとして。それらをメインラインの ISO C++ 標準にマージすることで、std::atomic:std::atomic<std::shared_ptr> の部分的なテンプレート特殊化になりました。 、および std::atomic<std::weak_ptr> .

    したがって、std::shared_ptr<T> のアトミック操作は C++20 では非推奨です。

    次は?

    C++20 では、スレッドを協調的に中断できます。それが何を意味するのか、次の記事でお見せしましょう。


    No