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
であるとします。
0
にアトミックにdecrefします。1
にアトミックにインクリメントします。delete
を呼び出します。1
と見なし、管理対象オブジェクトポインタにアクセスします...SEGFAULT!Refcountが0に達してオブジェクトが削除されるまでの間のデータ競合を妨げるものは何もないため、このシナリオの発生を妨げるものが何であるか理解できません。 refcountの宣言とdelete
の呼び出しは、2つの別個の非アトミック操作です。では、これはロックなしでどのように可能ですか?
Shared_ptrが提供するスレッドセーフを過大評価している可能性があります。
アトミック参照カウントの本質は、shared_ptr
(同じオブジェクトを管理している)の2つの異なるインスタンスにアクセスすることを保証することです/変更された場合、競合状態は発生しません。ただし、2つのスレッドが同じshared_ptr
オブジェクトにアクセスする場合(そのうちの1つが書き込みである場合)、shared_ptr
はスレッドセーフを保証しません。一例は、例えばです。一方のスレッドがポインターを逆参照し、もう一方のスレッドがポインターをリセットする場合。
つまり、shared_ptr
が保証する唯一のことは、shared_ptrの単一インスタンスで競合がない限り、二重削除やリークが発生しないことです(これは、にアクセスすることもありません)。スレッドセーフを指すオブジェクト)
その結果、shared_ptrのコピーを作成することも、それを同時に削除/リセットできるスレッドが他にない場合にのみ安全です(つまり、内部的に同期されていないと言うこともできます)。これはあなたが説明するシナリオです。
もう一度繰り返すには:singleshared_ptr
instancefrom複数のスレッドこれらのアクセスの1つがポインタへの書き込みである場合はまだ競合状態です。
あなたがしたい場合例えばstd::shared_ptr
をスレッドセーフな方法でコピーするには、すべてのロードとストアがstd::atomic_...
に特化した shared_ptr
操作を介して行われるようにする必要があります。
そのような状況は決して起こりません。共有ポインタの参照カウントが0に達すると、それへの最後の参照が削除され、ポインタを安全に削除できます。コピーできるインスタンスが残っていないため、共有ポインターへの別の参照を作成する方法はありません。
実装はそのような保証を提供または要求しません。あなたが説明している振る舞いの回避は、通常_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
}
_
スレッドBはすでに増分された参照カウントで作成されているはずなので、シナリオは不可能です。スレッドBは、最初に行うように参照カウントをインクリメントするべきではありません。
スレッドAがスレッドBを生成するとします。スレッドAは、スレッドの安全性を保証するために、スレッドを作成する前にオブジェクトの参照カウントをインクリメントする責任があります。スレッドBは、終了時にのみreleaseを呼び出す必要があります。
スレッドAが参照カウントをインクリメントせずにスレッドBを作成すると、説明したように悪いことが起こる可能性があります。
スレッドB:refcountをアトミックに1に増やします。
不可能な。参照カウントを1にインクリメントするには、参照カウントをゼロにする必要があります。しかし、参照カウントがゼロの場合、スレッドBはどのようにオブジェクトにアクセスしていますか?
スレッドBにオブジェクトへの参照があるか、ないかのどちらかです。含まれている場合、参照カウントをゼロにすることはできません。そうでない場合、そのオブジェクトへの参照がないのに、なぜスマートポインターによって管理されているオブジェクトをいじるのですか?
std::shared_ptr
の場合、参照カウントの変更はスレッドセーフですが、 `shared_ptrのコンテンツへのアクセスではありません。
boost::intrusive_ptr<X>
に関しては、これは答えではありません。