web-dev-qa-db-ja.com

std :: shared_ptrとstd :: experimental :: atomic_shared_ptrの違いは何ですか?

Antony Williamsによる following の記事を読み、std::shared_ptrのアトミック共有カウントに加えてstd::experimental::atomic_shared_ptr共有オブジェクトへの実際のポインタもアトミックですか?

しかし、 C++ Concurrency に関するAntonyの本に記載されているlock_free_stackの参照カウントバージョンについて読むと、std::shared_ptrにも同じことが当てはまるようです。 std::atomic_loadstd::atomic_compare_exchnage_weakなどの関数は、std::shared_ptrのインスタンスに適用されます。

template <class T>
class lock_free_stack
{
public:
  void Push(const T& data)
  {
    const std::shared_ptr<node> new_node = std::make_shared<node>(data);
    new_node->next = std::atomic_load(&head_);
    while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
  }

  std::shared_ptr<T> pop()
  {
    std::shared_ptr<node> old_head = std::atomic_load(&head_);
    while(old_head &&
          !std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
    return old_head ? old_head->data : std::shared_ptr<T>();
  }

private:
  struct node
  {
    std::shared_ptr<T> data;
    std::shared_ptr<node> next;

    node(const T& data_) : data(std::make_shared<T>(data_)) {}
  };

private:
  std::shared_ptr<node> head_;
};

この2種類のスマートポインターの正確な違いは何ですか。また、std::shared_ptrインスタンスのポインターがアトミックでない場合、上記のロックフリースタックの実装が可能なのはなぜですか。

22
bobeff

shared_ptrのアトミックな「もの」は、共有ポインタ自体ではなく、それが指す制御ブロックです。つまり、複数のスレッドでshared_ptrを変更しない限り、問題はありません。 copying a shared_ptrは制御ブロックのみを変更し、shared_ptr自体は変更しないことに注意してください。

std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
   std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block 
}

たとえば、複数のスレッドから異なる値を割り当てるなど、共有ポインタ自体を変更することは、データ競合です。

std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
   ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
   ptr = std::make_shared<int>(20);
});    

ここでは、複数のスレッドからの異なる値を指すようにして、制御ブロック(問題ありません)と共有ポインター自体を変更しています。これは大丈夫ではありません。

この問題の解決策は、shared_ptrをロックでラップすることですが、この解決策は、競合下ではそれほどスケーラブルではなく、ある意味では、標準の共有ポインターの自動感覚を失います。

別の解決策は、std::atomic_compare_exchange_weakなど、引用した標準関数を使用することです。これにより、共有ポインタを同期する作業が手動ではなくなりますが、これは望ましくありません。

これが、アトミックな共有ポインターの出番です。データの競合を心配することなく、ロックを使用せずに、共有ポインタを複数のスレッドから変更できます。スタンドアロン関数はメンバー関数になり、ユーザーにとってそれらの使用ははるかに自然になります。この種のポインタは、ロックのないデータ構造に非常に役立ちます。

23
David Haim

N4162(pdf)、アトミックスマートポインターの提案は、適切な説明があります。関連部分の引用は次のとおりです。

整合性。私の知る限りでは、[util.smartptr.shared.atomic]関数は、atomicタイプを介して利用できない、標準で唯一のアトミック操作です。また、shared_ptr以外のすべての型について、atomic_* Cスタイルの関数ではなく、C++でアトミック型を使用するようにプログラマーに指示します。その理由の一部は...

正しさ。無料の関数を使用すると、デフォルトでコードがエラーが発生しやすくなります。変数宣言自体にatomicを1回書き込み、すべてのアクセスがアトミックになることを知っておくと、オブジェクトの__everyの使用でatomic_*演算を使用することを覚える必要がなく、はるかに優れています。明白に明白な読みでさえ。後者のスタイルはエラーが発生しやすいです。たとえば、「間違っている」とは、単に空白文字(たとえば、atomic_load(&head)の代わりにhead)を書き込むことを意味するため、このスタイルでは、変数のすべての使用が「デフォルトで間違っています」。 atomic_*の呼び出しを1か所に書き忘れても、コードはエラーや警告なしで正常にコンパイルされ、ほとんどのテストに合格することを含め、「機能しているように見えます」が、通常は未定義の動作を伴うサイレントレースが含まれます。多くの場合/通常は現場で、再現が困難な断続的な障害として表面化し、場合によっては悪用可能な脆弱性も予想されます。これらのエラーのクラスは、変数atomicを宣言するだけで排除されます。デフォルトで安全であり、同じバグのセットを書き込むには、明示的な空白以外のコード(明示的なmemory_order_*引数、通常はreinterpret_casting)が必要なためです。

パフォーマンス。特殊タイプとしてのatomic_shared_ptr<>は、[util.smartptr.shared.atomic]の関数よりも効率的に重要な利点があります。これは、atomic_flagの場合と同様に、内部スピンロック用の追加のatomic<bigstruct>(または同様の)を格納するだけです。対照的に、shared_ptrsの大部分はアトミックに使用されない場合でも、既存のスタンドアロン関数は任意のshared_ptrオブジェクトで使用できる必要があります。これにより、フリー関数は本質的に効率が低下します。たとえば、実装では、すべてのshared_ptrが内部スピンロック変数のオーバーヘッドを運ぶ必要があります(同時実行性は向上しますが、shared_ptrごとに大幅なオーバーヘッドが発生します)。そうでない場合、ライブラリは、実際に使用されるshared_ptrsの追加情報を格納するためのルックアサイドデータ構造を維持する必要があります。アトミックに、または(最悪かつ実際には明らかに一般的)ライブラリはグローバルスピンロックを使用する必要があります。

6
cpplearner

_shared_ptr_でstd::atomic_load()またはstd::atomic_compare_exchange_weak()を呼び出すことは、atomic_shared_ptr::load()またはatomic_shared_ptr::atomic_compare_exchange_weak()を呼び出すことと機能的に同等です。 2つの間にパフォーマンスの違いがあってはなりません。 _atomic_shared_ptr_でstd::atomic_load()またはstd::atomic_compare_exchange_weak()を呼び出すと、構文的に冗長になり、パフォーマンスが低下する場合と低下する場合があります。

5
atb

atomic_shared_ptr はAPIの改良版です。 shared_ptrはすでにアトミック操作をサポートしていますが、適切な アトミック非メンバー関数 を使用する場合のみです。非アトミックな操作は引き続き利用可能であり、不注意なプログラマが偶然に呼び出すのは簡単すぎるため、これはエラーが発生しやすくなります。 atomic_shared_ptrは、非アトミック操作を公開しないため、エラーが発生しにくくなります。

shared_ptratomic_shared_ptrは異なるAPIを公開していますが、必ずしも別々に実装する必要はありません。 shared_ptrは、atomic_shared_ptrによって公開されるすべての操作をすでにサポートしています。そうは言っても、shared_ptrのアトミック操作は非アトミック操作もサポートする必要があるため、可能な限り効率的ではありません。したがって、atomic_shared_ptrを異なる方法で実装できるパフォーマンス上の理由があります。これは単一責任の原則に関連しています。 「複数の異なる目的を持つエンティティは、機能のさまざまな領域間の部分的なオーバーラップにより、それぞれを明確に実装するために必要なビジョンが不鮮明になるため、特定の目的のために機能不全のインターフェースを提供することがよくあります。」 (Sutter&Alexandrescu 2005、C++コーディング標準

4
Oktalist