web-dev-qa-db-ja.com

std :: shared_ptr :: unique()が推奨されないのはなぜですか?

C++ 17で非推奨となった理由であるstd::shared_ptr::unique()の技術的な問題は何ですか?

cppreference.com によると、std::shared_ptr::unique()はC++ 17では非推奨

_use_count_はマルチスレッド環境での概算にすぎないため、この関数はC++ 17以降では非推奨です。

これがuse_count() > 1についても当てはまることを理解しました。参照を保持している間、他の誰かが同時に手放したり、新しいコピーを作成したりする可能性があります。

しかし、use_count()が1を返す場合(これがunique()を呼び出すときに興味があることです)、その値をきちんと変更できる他のスレッドがないため、期待しますこれは安全であるべきだと:

_if (myPtr && myPtr.unique()) {
    //Modify *myPtr
}
_

私自身の検索の結果:

私はこのドキュメントを見つけました: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0521r0.html これはに応じて非推奨を提案しますC++ 17 CDコメントCA 14ですが、コメント自体は見つかりませんでした。

代わりに、その論文は以下を含むいくつかのメモを追加することを提案しました:

注:複数のスレッドがuse_count()の戻り値に影響を与える可能性がある場合、結果は概数として扱う必要があります。 特に、use_count() == 1は、以前に破棄された_shared_ptr_を介したアクセスが何らかの意味で完了したことを意味しません。—終了注意

これは、use_count()が現在指定されている方法(同期が保証されていないため)の場合に当てはまる可能性があることを理解していますが、そのような同期を指定するだけでなく、上記のパターンを安全にするための解決策がなかったのはなぜですか?そのような同期を許可しない(または、それを非常に高価なものにする)基本的な制限があった場合、デストラクタを正しく実装するにはどうすればよいでしょうか?

更新:

@ alexeykuzmin0と@rubenvbによって提示された明らかなケースを見落としました。これまでのところ、他のスレッド自体にはアクセスできない_shared_ptr_のインスタンスでのみunique()を使用したためです。したがって、その特定のインスタンスが際どい方法でコピーされる危険はありませんでした。

unique()のすべての使用例は、異なる_shared_ptr_で発生したすべてのことと同期することが保証されている限り機能すると考えているため、CA 14が正確に何であるかについて聞きたいと思います。他のスレッドのインスタンス。ですから、私にとってはまだ便利なツールのようですが、ここでは基本的なことを見落としているかもしれません。

私が考えていることを説明するために、次のことを考慮してください。

_class MemoryCache {
public:
    MemoryCache(size_t size)
        : _cache(size)
    {
        for (auto& ptr : _cache) {
            ptr = std::make_shared<std::array<uint8_t, 256>>();
        }
    }

    // the returned chunk of memory might be passed to a different thread(s),
    // but the function is never accessed from two threads at the same time
    std::shared_ptr<std::array<uint8_t,256>> getChunk()
    {
        auto it = std::find_if(_cache.begin(), _cache.end(), [](auto& ptr) { return ptr.unique(); });
        if (it != _cache.end()) {
            //memory is no longer used by previous user, so it can be given to someone else
            return *it;
        } else {
            return{};
        }
    }
private:
    std::vector<std::shared_ptr<std::array<uint8_t, 256>>> _cache;
};
_

何か問題がありますか(unique()が他のコピーのデストラクタと実際に同期する場合)?

31
MikeMB

私は P0521R が_shared_ptr_をスレッド間同期として誤用することにより潜在的にデータ競合を解決すると思います。 use_count()は信頼できないrefcount値を返すため、マルチスレッドの場合、unique()メンバー関数は役に立たなくなります。

_int main() {
  int result = 0;
  auto sp1 = std::make_shared<int>(0);  // refcount: 1

  // Start another thread
  std::thread another_thread([&result, sp2 = sp1]{  // refcount: 1 -> 2
    result = 42;  // [W] store to result
    // [D] expire sp2 scope, and refcount: 2 -> 1
  });

  // Do multithreading stuff:
  //   Other threads may concurrently increment/decrement refcounf.

  if (sp1.unique()) {      // [U] refcount == 1?
    assert(result == 42);  // [R] read from result
    // This [R] read action cause data race w.r.t [W] write action.
  }

  another_thread.join();
  // Side note: thread termination and join() member function
  // have happens-before relationship, so [W] happens-before [R]
  // and there is no data race on following read action.
  assert(result == 42);
}
_

メンバー関数unique()には同期効果がなく、[D] _shared_ptr_のデストラクタから[U]へのhappens-before関係はありません] unique()を呼び出します。したがって、[W]⇒[D]⇒[U]⇒[R]と[W]⇒[R]の関係は期待できません。 (「⇒」は発生前の関係を示します)。


編集済み:2つの関連するLWG問題が見つかりました。 LWG2434。shared_ptr :: use_count()が効率的LWG2776。shared_ptr unique()およびuse_count() 。これは単なる憶測ですが、WG21委員会はC++標準ライブラリの既存の実装を優先するため、C++ 1zでの動作を体系化しています。

LWG2434 引用(鉱山を強調):

_shared_ptr_および_weak_ptr_には、use_count()が非効率的である可能性があるという注記があります。 これは、reflinked実装を確認する試みです(たとえば、Lokiスマートポインタで使用できます)。 ただし、特にC++ 11がマルチスレッドの存在を認識した後は、reflinkingを使用する_shared_ptr_の実装はありません。誰もがアトミックな参照カウントを使用しているため、use_count()はアトミックなロードです

LWG2776 引用(私の強調):

LWG 2434による_shared_ptr_のuse_count()およびunique()の「デバッグのみ」制限の削除により、バグが導入されました。 unique()が有用で信頼性の高い値を生成するためには、別の参照を介した以前のアクセスがunique()の正常な呼び出し側から見えるようにする同期句が必要です。 現在の多くの実装では、緩和されたロードを使用しており、標準で規定されていないため、この保証を提供していません。OKだったデバッグ/ヒントの使用法。それがなければ仕様は不明確でおそらく誤解を招くでしょう。

[...]

私はuse_count()を実際のカウントの信頼できないヒントを提供するだけとして指定することを好みます(デバッグのみを言う別の方法)。または、JFが提案したように、非推奨にします。 実質的にフェンシングを追加しないと、use_count()の信頼性を高めることはできません。 use_count() == 2を待っている誰かが別のスレッドがそこまで到達したと判断することは本当に望まない。残念ながら、現在、それが間違いであることを明確にするために何かを言うことはないと思います。

これは、use_count()が通常_memory_order_relaxed_を使用し、uniqueがuse_count()に関して指定も実装もされていないことを意味します。

6
yohjp

次のコードを検討してください。

_// global variable
std::shared_ptr<int> s = std::make_shared<int>();

// thread 1
if (s && s.unique()) {
    // modify *s
}

// thread 2
auto s2 = s;
_

ここに古典的な競合状態があります:_s2_は、スレッド2のsのコピーとして作成される場合とされない場合がありますが、スレッド1はif内にあります。

unique() == trueは、同じメモリを指す_shared_ptr_が誰にもないことを意味しますが、他のスレッドが初期_shared_ptr_に直接またはポインタや参照を介してアクセスできないことを意味しません。

12
alexeykuzmin0

閲覧の喜びのために: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0488r0.pdf

このドキュメントには、イサクア会議のNB(National Body))コメントがすべて含まれています。

Shared_ptrのuse_count()とunique()の「デバッグのみ」の制限を削除すると、バグが発生しました。unique()が有用で信頼できる値を生成するには、別の参照を介して事前にアクセスできるようにするための同期句が必要ですunique()の成功した呼び出し元に表示されます。現在の実装の多くは、緩和されたロードを使用しており、規格には明記されていないため、この保証はありません。 OKだったデバッグ/ヒントの使用法。それがなければ、仕様は不明確で誤解を招きます。

3
Mikel F