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

C++20 にはアトミック スマート ポインターが含まれます。正確には、std::atomic_shared_ptr と std::atomic_weak_ptr を取得します。しかし、なぜ? std::shared_ptr と std::weak_ptr は既にスレッドセーフです。並べ替え。詳細に飛び込みましょう。

始める前に、少し寄り道をしたいと思います。この回り道は、std::shared_ptr が明確に定義されたマルチスレッド セマンティックを持ち、それを知って使用することがいかに重要であるかを強調するだけです。マルチスレッドの観点から見ると、std::shared_ptr は、マルチスレッド プログラムでは使用しないこの種のデータ構造です。それらは定義上、共有され、変更可能です。したがって、それらはデータ競合の理想的な候補であり、したがって未定義の動作の理想的な候補です。一方、最新の C++ には次のガイドラインがあります。メモリに触れないでください。つまり、マルチスレッド プログラムではスマート ポインターを使用します。

ハーフスレッドセーフ

C++ のセミナーでよく質問されるのは、スマート ポインターはスレッドセーフですか?私の定義した答えはイエスとノーです。なんで? std::shared_ptr は、制御ブロックとそのリソースで構成されます。はい、制御ブロックはスレッドセーフです。いいえ、リソースへのアクセスはスレッドセーフではありません。つまり、参照カウンターの変更はアトミック操作であり、リソースが 1 回だけ削除されることが保証されます。これらはすべて、std::shared_ptr が提供する保証です。

std::shared_ptr が提供するアサーションは、Boost によって記述されます。

<オール>
  • shared_ptr インスタンスは、複数のスレッドで同時に「読み取る」(const 操作のみを使用してアクセスする) ことができます。
  • 異なる shared_ptr インスタンスは、複数のスレッドによって同時に「書き込み」(operator=や reset などの変更可能な操作を使用してアクセス) できます (これらのインスタンスがコピーであり、その下で同じ参照カウントを共有している場合でも)。
  • 2 つのステートメントを明確にするために、簡単な例を示します。スレッドで std::shared_ptr をコピーすると、すべて問題ありません。

    std::shared_ptr<int> ptr = std::make_shared<int>(2011);
    
    for (auto i= 0; i<10; i++){
     std::thread([ptr]{ (1)
     std::shared_ptr<int> localPtr(ptr); (2)
     localPtr= std::make_shared<int>(2014); (3)
     }).detach(); 
    }
    

    まずは(2)へ。 std::shared_ptr localPtr のコピー構築を使用することにより、制御ブロックのみが使用されます。それはスレッドセーフです。 (3) はもう少し興味深いものです。 localPtr (3) は新しい std::shared_ptr に設定されます。これは、マルチスレッドの観点からは問題ありません。Die lambda-function (1) はコピーによって ptr をバインドします。したがって、localPtr の変更はコピーに対して行われます。

    std::shared_ptr を参照すると、話は劇的に変わります。

    std::shared_ptr<int> ptr = std::make_shared<int>(2011); 
    
    for (auto i= 0; i<10; i++){
     std::thread([&ptr]{ (1)
     ptr= std::make_shared<int>(2014); (2)
     }).detach(); 
    }
    

    ラムダ関数は、参照によって std::shared_ptr ptr をバインドします (1)。したがって、割り当て (2) はリソースの競合状態であり、プログラムは未定義の動作をします。

    そう簡単に手に入れられなかったのも事実です。 std::shared_ptr は、マルチスレッド環境で特別な注意を払う必要があります。彼らはとても特別です。これらは、アトミック操作が存在する C+ の唯一の非アトミック データ型です。

    std::shared_ptr のアトミック操作

    std::shared_ptr のアトミック操作のロード、ストア、比較、および交換の特殊化があります。明示的なバリアントを使用することで、メモリ モデルを指定することもできます。 std::shared_ptr の無料のアトミック操作は次のとおりです。

    std::atomic_is_lock_free(std::shared_ptr)
    std::atomic_load(std::shared_ptr)
    std::atomic_load_explicit(std::shared_ptr)
    std::atomic_store(std::shared_ptr)
    std::atomic_store_explicit(std::shared_ptr)
    std::atomic_exchange(std::shared_ptr)
    std::atomic_exchange_explicit(std::shared_ptr)
    std::atomic_compare_exchange_weak(std::shared_ptr)
    std::atomic_compare_exchange_strong(std::shared_ptr)
    std::atomic_compare_exchange_weak_explicit(std::shared_ptr)
    std::atomic_compare_exchange_strong_explicit(std::shared_ptr)
    

    詳細については、cppreference.com をご覧ください。これで、スレッドセーフな方法で、参照によって制限された std::shared_ptr を非常に簡単に変更できるようになりました。

    std::shared_ptr<int> ptr = std::make_shared<int>(2011);
    
    for (auto i =0;i<10;i++){
     std::thread([&ptr]{ 
     auto localPtr= std::make_shared<int>(2014);
     std::atomic_store(&ptr, localPtr); (1)
     }).detach(); 
    }
    

    std::shared_ptr ptr (1) の更新はスレッドセーフです。すべては順調です? いいえ .最後に、新しいアトミック スマート ポインターについて説明します。

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

    アトミック スマート ポインターの提案 N4162 は、現在の実装の欠陥に直接対処しています。欠点は、一貫性、正確性、およびパフォーマンスの 3 点に要約されます。以上、3点の概要でした。詳細については、提案を読む必要があります。

    一貫性: std::shared_ptr のアトミック操作は、非アトミック データ型に対する唯一のアトミック操作です。

    正確さ: フリー アトミック操作の使用法は、正しい使用法が規律に基づいているため、非常にエラーが発生しやすくなります。最後の例のように、アトミック操作の使用を忘れがちです。std::atomic_store(&ptr, localPtr) の代わりに prt=localPtr を使用します。その結果、データ競合のために未定義の動作が発生します。代わりにアトミック スマート ポインターを使用した場合、コンパイラはそれを許可しません。

    パフォーマンス: std::atomic_shared_ptr と std::atomic_weak_ptr には、無料の atom_* 関数よりも大きな利点があります。それらは特殊なユース ケースのマルチスレッド用に設計されており、たとえば std::atomic_flag を安価なスピンロックの一種として持つことができます。 (スピンロックと std::atomic_flag の詳細については、The Atomic Flag の投稿を参照してください)。もちろん、可能性のあるマルチスレッドのユースケースに対して、各 std::shared_ptr または std::weak_ptr に std::atomic_flag を配置してそれらをスレッドセーフにすることはあまり意味がありません。しかし、両方がマルチスレッドのユース ケース用のスピンロックを持っていて、アトミック スマート ポインターが存在しない場合は、その結果になります。これは、std::shared_ptr と std::weak_ptr が特別なユース ケースに最適化されていることを意味します。

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

    スレッド セーフな単一リンク リスト

    template<typename T> class concurrent_stack {
     struct Node { T t; shared_ptr<Node> next; };
     atomic_shared_ptr<Node> head;
     // in C++11: remove “atomic_” and remember to use the special
     // functions every time you touch the variable
     concurrent_stack( concurrent_stack &) =delete;
     void operator=(concurrent_stack&) =delete;
    
    public:
     concurrent_stack() =default;
     ~concurrent_stack() =default;
     class reference {
     shared_ptr<Node> p;
     public:
     reference(shared_ptr<Node> p_) : p{p_} { }
     T& operator* () { return p->t; }
     T* operator->() { return &p->t; }
     };
    
     auto find( T t ) const {
     auto p = head.load(); // in C++11: atomic_load(&head)
     while( p && p->t != t )
     p = p->next;
     return reference(move(p));
     }
     auto front() const {
     return reference(head); // in C++11: atomic_load(&head)
     }
     void push_front( T t ) {
     auto p = make_shared<Node>();
     p->t = t;
     p->next = head; // in C++11: atomic_load(&head)
     while( !head.compare_exchange_weak(p->next, p) ){ }
     // in C++11: atomic_compare_exchange_weak(&head, &p->next, p);
     }
     void pop_front() {
     auto p = head.load();
     while( p && !head.compare_exchange_weak(p, p->next) ){ }
     // in C++11: atomic_compare_exchange_weak(&head, &p, p->next);
     }
    };
    

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

    次は?

    C++11 は、promise と futures の形で高度なマルチスレッドの概念を採用しています。より多くのスレッドを提供しますが、大きな欠点があります。 C++11 の先物は構成できません。 C++20 の Extended future は、この欠点を克服します。どのように?次の投稿を読んでください。