web-dev-qa-db-ja.com

原子参照カウント

std::shared_ptrのように、スレッドセーフなアトミック参照カウントがどのように機能するかを正確に理解しようとしています。つまり、基本的な概念は単純ですが、decrefとdeleteが競合状態を回避する方法については本当に混乱しています。

この Boostのチュートリアル は、Boostアトミックライブラリ(またはC++ 11アトミックライブラリ)を使用して、アトミックスレッドセーフな参照カウントシステムを実装する方法を示しています。

#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>

class X {
public:
  typedef boost::intrusive_ptr<X> pointer;
  X() : refcount_(0) {}

private:
  mutable boost::atomic<int> refcount_;
  friend void intrusive_ptr_add_ref(const X * x)
  {
    x->refcount_.fetch_add(1, boost::memory_order_relaxed);
  }
  friend void intrusive_ptr_release(const X * x)
  {
    if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
      boost::atomic_thread_fence(boost::memory_order_acquire);
      delete x;
    }
  }
};

さて、私は一般的な考えを理解します。しかし、次のシナリオが不可能な理由がわかりません。

Refcountが現在1であるとします。

  1. スレッドA:refcountを0にアトミックにdecrefします。
  2. スレッドB:refcountを1にアトミックにインクリメントします。
  3. スレッドA:管理対象オブジェクトポインタでdeleteを呼び出します。
  4. スレッドB:refcountを1と見なし、管理対象オブジェクトポインタにアクセスします...SEGFAULT!

Refcountが0に達してオブジェクトが削除されるまでののデータ競合を妨げるものは何もないため、このシナリオの発生を妨げるものが何であるか理解できません。 refcountの宣言とdeleteの呼び出しは、2つの別個の非アトミック操作です。では、これはロックなしでどのように可能ですか?

17
Siler

Shared_ptrが提供するスレッドセーフを過大評価している可能性があります。

アトミック参照カウントの本質は、shared_ptr(同じオブジェクトを管理している)の2つの異なるインスタンスにアクセスすることを保証することです/変更された場合、競合状態は発生しません。ただし、2つのスレッドが同じshared_ptrオブジェクトにアクセスする場合(そのうちの1つが書き込みである場合)、shared_ptrはスレッドセーフを保証しません。一例は、例えばです。一方のスレッドがポインターを逆参照し、もう一方のスレッドがポインターをリセットする場合。
つまり、shared_ptrが保証する唯一のことは、shared_ptrの単一インスタンスで競合がない限り、二重削除やリークが発生しないことです(これは、にアクセスすることもありません)。スレッドセーフを指すオブジェクト)

その結果、shared_ptrのコピーを作成することも、それを同時に削除/リセットできるスレッドが他にない場合にのみ安全です(つまり、内部的に同期されていないと言うこともできます)。これはあなたが説明するシナリオです。

もう一度繰り返すには:singleshared_ptrinstancefrom複数のスレッドこれらのアクセスの1つがポインタへの書き込みである場合はまだ競合状態です

あなたがしたい場合例えばstd::shared_ptrをスレッドセーフな方法でコピーするには、すべてのロードとストアがstd::atomic_...に特化した shared_ptr 操作を介して行われるようにする必要があります。

23
MikeMB

そのような状況は決して起こりません。共有ポインタの参照カウントが0に達すると、それへの最後の参照が削除され、ポインタを安全に削除できます。コピーできるインスタンスが残っていないため、共有ポインターへの別の参照を作成する方法はありません。

6
NathanOliver

実装はそのような保証を提供または要求しません。あなたが説明している振る舞いの回避は、通常_std::shared_ptr_などのRAIIクラスを介して行われるカウントされた参照の適切な管理に基づいています。重要なのは、スコープ間で生のポインターを渡すことを完全に回避することです。オブジェクトへのポインタを格納または保持する関数は、参照カウントを適切にインクリメントできるように、共有ポインタを使用する必要があります。

_void f(shared_ptr p) {
   x(p); // pass as a shared ptr
   y(p.get()); // pass raw pointer
}
_

この関数には_shared_ptr_が渡されたため、refcountはすでに1+でした。ローカルインスタンスpは、コピー割り当て中にref_countをバンプするはずです。 xを呼び出したときに、値を渡した場合は、別の参照を作成しました。 const refを通過した場合、現在の参照カウントを保持しました。 non-const refを渡した場合、x()が参照を解放し、yがnullで呼び出される可能性があります。

x()が生のポインタを格納/保持する場合、問題が発生している可能性があります。関数が戻ると、refcountが0に達し、オブジェクトが破棄される可能性があります。これは、参照カウントを正しく維持していないための私たちのせいです。

考えてみましょう:

_template<typename T>
void test()
{
    shared_ptr<T> p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:2
    } // ~q -> rc:1
    use(p.get()); // valid
} // ~p -> rc:0 -> delete
_

vs

_template<typename T>
void test()
{
    T* p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:1
    } // ~q -> rc:0 -> delete
    use(p); // bad: accessing deleted object
}
_
3
kfsone

スレッドBはすでに増分された参照カウントで作成されているはずなので、シナリオは不可能です。スレッドBは、最初に行うように参照カウントをインクリメントするべきではありません。

スレッドAがスレッドBを生成するとします。スレッドAは、スレッドの安全性を保証するために、スレッドを作成する前にオブジェクトの参照カウントをインクリメントする責任があります。スレッドBは、終了時にのみreleaseを呼び出す必要があります。

スレッドAが参照カウントをインクリメントせずにスレッドBを作成すると、説明したように悪いことが起こる可能性があります。

3
Paladine

スレッドB:refcountをアトミックに1に増やします。

不可能な。参照カウントを1にインクリメントするには、参照カウントをゼロにする必要があります。しかし、参照カウントがゼロの場合、スレッドBはどのようにオブジェクトにアクセスしていますか?

スレッドBにオブジェクトへの参照があるか、ないかのどちらかです。含まれている場合、参照カウントをゼロにすることはできません。そうでない場合、そのオブジェクトへの参照がないのに、なぜスマートポインターによって管理されているオブジェクトをいじるのですか?

2
David Schwartz

std::shared_ptrの場合、参照カウントの変更はスレッドセーフですが、 `shared_ptrのコンテンツへのアクセスではありません。

boost::intrusive_ptr<X>に関しては、これは答えではありません。

1
user2249683