yohhoyの日記

技術的メモをしていきたい日記

shared_ptr参照カウントとデータ競合

C++1z(C++17)標準ライブラリでは、スマートポインタstd::shared_ptr<T>uniqueメンバ関数は非推奨(deprecated)とされる*1。上位互換となるuse_countメンバ関数は残存するが、スレッド間同期には関与しないという要件が明確化され、マルチスレッド実行ではその戻り値は近似(approximate)となる旨のNoteが追加される。

2019-09-11追記:C++2a(C++20)では P0619R4 が採択され、shared_ptr<T>::unique()メンバ関数は削除(remove)される。

まとめ:

  • shared_ptr参照カウントを利用したスレッド間同期は行わないこと。データ競合(data race)のリスクがある。
  • shared_ptr<T>::uniqueメンバ関数を利用しないこと。C++1zから非推奨(deprecated)。
  • shared_ptr<T>::use_countメンバ関数は、デバッグ用途にのみ利用する。マルチスレッド処理においては信頼できない値を返す可能性がある。

C++14以前

C++14時点のuniqueメンバ関数は、shared_ptrオブジェクトの参照先が唯一(参照カウント==1)か否かを判定する最適化として用意されていた。C++14 20.8.2.2.5/p7-10より引用。

long use_count() const noexcept;
7 Returns: the number of shared_ptr objects, *this included, that share ownership with *this, or 0 when *this is empty.
8 [Note: use_count() is not necessarily efficient. -- end note]

bool unique() const noexcept;
9 Returns: use_count() == 1.
10 [Note: unique() may be faster than use_count(). If you are using unique() to implement copy on write, do not rely on a specific value when get() == 0. -- end note]

これは参照リンク方式によるshared_ptr内部実装*2を想定したものだったが、実際には参照カウンタ方式のC++標準ライブラリ実装しか存在しない。LWG2434より引用(下線部は強調)。

shared_ptr and weak_ptr have Notes that their use_count() might be inefficient. This is an attempt to acknowledge reflinked implementations (which can be used by Loki smart pointers, for example). However, there aren't any shared_ptr implementations that use reflinking, especially after C++11 recognized the existence of multithreading. Everyone uses atomic refcounts, so use_count() is just an atomic load.

C++1z以降

既存のC++標準ライブラリ実装がすべて参照カウント方式を採用しており、use_countメンバ関数はメモリバリア効果を持たない relaxed load 操作により実装されている。LWG2776より一部引用(下線部は強調)。

The removal of the "debug only" restriction for use_count() and unique() in shared_ptr by LWG 2434 introduced a bug. In order for unique() to produce a useful and reliable value, it needs a synchronize clause to ensure that prior accesses through another reference are visible to the successful caller of unique(). Many current implementations use a relaxed load, and do not provide this guarantee, since it's not stated in the standard. For debug/hint usage that was OK. Without it the specification is unclear and probably misleading.

このような状況をうけC++1z標準ライブラリでは、use_count関数が「スレッド間同期に関与しないこと」と「マルチスレッド実行の下では信頼できない値を返しうること」を明文化する。また存在価値のなくなったuniqueメンバ関数は非推奨(deprecated)とされる。提案文書P0521R0よりuse_countメンバ関数仕様に追加されるWordingを引用(下線部は強調)。

Synchronization: None.
[Note: get() == nullptr does not imply a specific return value of use_count(). -- end note]
[Note: weak_ptr<T>::lock() can affect the return value of use_count(). -- end note]
[Note: When multiple threads can affect the return value of use_count(), the result should be treated as approximate. In particular, use_count() == 1 does not imply that accesses through a previously destroyed shared_ptr have in any sense completed. -- end note]

下記のようにshared_ptr参照カウンタをスレッド間同期に使うコードは、データ競合(data race)による未定義動作(undefined behavior)を引き起こす。

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);
}

関連URL

*1:2016年12月現在の(PDF)N4618 Working Draftからはuniqueメンバ関数が削除され、D.13 Deprecated shared_ptr observersに移動している。

*2:同一オブジェクトを参照する shared_ptr オブジェクト間をリンクリストで相互参照しあう方式。use_count メンバ関数ではリンクリストを辿って shared_ptr オブジェクト数を数える必要があるが、shared_ptr オブジェクトが唯一、つまりリンクリストが1ノードの判定は効率的に行える。