5

次のコードを見てください。

#include <pthread.h>
#include <boost/atomic.hpp>

class ReferenceCounted {
  public:
    ReferenceCounted() : ref_count_(1) {}

    void reserve() {
      ref_count_.fetch_add(1, boost::memory_order_relaxed);
    }

    void release() {
      if (ref_count_.fetch_sub(1, boost::memory_order_release) == 1) {
        boost::atomic_thread_fence(boost::memory_order_acquire);
        delete this;
      }
    }

  private:
    boost::atomic<int> ref_count_;
};

void* Thread1(void* x) {
  static_cast<ReferenceCounted*>(x)->release();
  return NULL;
}

void* Thread2(void* x) {
  static_cast<ReferenceCounted*>(x)->release();
  return NULL;
}

int main() {
  ReferenceCounted* obj = new ReferenceCounted();
  obj->reserve(); // for Thread1
  obj->reserve(); // for Thread2
  obj->release(); // for the main()
  pthread_t t[2];
  pthread_create(&t[0], NULL, Thread1, obj);
  pthread_create(&t[1], NULL, Thread2, obj);
  pthread_join(t[0], NULL);
  pthread_join(t[1], NULL);
}

これは、 Boost.Atomicの参照カウントの例に多少似ています。

主な違いは、組み込みref_count_がコンストラクターで初期化さ1れること (コンストラクターが完了すると、ReferenceCountedオブジェクトへの参照が 1 つになる) と、コードが を使用しないことboost::intrusive_ptrです。

コードで使用したことで私を責めないでください。delete thisこれは、私が職場の大規模なコード ベースで使用しているパターンであり、今のところ私にできることは何もありません。

このコードをclang 3.5from trunk (詳細は下記) とThreadSanitizer (tsan v2) でコンパイルすると、ThreadSanitizer から次の出力が得られます。

WARNING: ThreadSanitizer: data race (pid=9871)
  Write of size 1 at 0x7d040000f7f0 by thread T2:
    #0 operator delete(void*) <null>:0 (a.out+0x00000004738b)
    #1 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:15 (a.out+0x0000000a2c06)
    #2 Thread2(void*) /home/A.Romanek/tmp/tsan/main.cpp:29 (a.out+0x0000000a2833)

  Previous atomic write of size 4 at 0x7d040000f7f0 by thread T1:
    #0 __tsan_atomic32_fetch_sub <null>:0 (a.out+0x0000000896b6)
    #1 boost::atomics::detail::base_atomic<int, int, 4u, true>::fetch_sub(int, boost::memory_order) volatile /home/A.Romanek/tmp/boost/boost_1_55_0/boost/atomic/detail/gcc-atomic.hpp:499 (a.out+0x0000000a3329)
    #2 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:13 (a.out+0x0000000a2a71)
    #3 Thread1(void*) /home/A.Romanek/tmp/tsan/main.cpp:24 (a.out+0x0000000a27d3)

  Location is heap block of size 4 at 0x7d040000f7f0 allocated by main thread:
    #0 operator new(unsigned long) <null>:0 (a.out+0x000000046e1d)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:34 (a.out+0x0000000a286f)

  Thread T2 (tid=9874, running) created by main thread at:
    #0 pthread_create <null>:0 (a.out+0x00000004a2d1)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:40 (a.out+0x0000000a294e)

  Thread T1 (tid=9873, finished) created by main thread at:
    #0 pthread_create <null>:0 (a.out+0x00000004a2d1)
    #1 main /home/A.Romanek/tmp/tsan/main.cpp:39 (a.out+0x0000000a2912)

SUMMARY: ThreadSanitizer: data race ??:0 operator delete(void*)
==================
ThreadSanitizer: reported 1 warnings

奇妙なことは、参照カウンターでアトミックデクリメントを行うときthread T1と同じメモリ位置にサイズ 1 の書き込みを行うことです。thread T2

前者の書き込みはどのように説明できますか? ReferenceCountedクラスのデストラクタによって実行されるクリーンアップですか?

偽陽性ですか?それともコードが間違っていますか?

私のセットアップは次のとおりです。

$ uname -a
Linux aromanek-laptop 3.13.0-29-generic #53-Ubuntu SMP Wed Jun 4 21:00:20 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ clang --version
Ubuntu clang version 3.5-1ubuntu1 (trunk) (based on LLVM 3.5)
Target: x86_64-pc-linux-gnu
Thread model: posix

コードは次のようにコンパイルされます。

clang++ main.cpp -I/home/A.Romanek/tmp/boost/boost_1_55_0 -pthread -fsanitize=thread -O0 -g -ggdb3 -fPIE -pie -fPIC

私のマシンでは、 の実装は、 ThreadSanitizer が理解していると主張する一連の関数にboost::atomic<T>解決されることに注意してください。__atomic_load_n

更新 1:clang 3.4最終リリースを使用する場合も同じことが起こります。

更新 2: libstdc++libc++の両方-std=c++11で同じ問題が発生します。<atomic>

4

2 に答える 2

3

これは偽陽性のようです。

thread_fenceメソッド内の は、フェンスが戻る前に-calls から発生release()するすべての未解決の書き込みを強制します。したがって、次の行の は、refcount の減少による以前の書き込みと競合することはありません。fetch_subdelete

本からの引用C++ Concurrency in Action :

解放操作は、その解放操作がフェンスと同じスレッドのフェンスの前にアトミック操作によって読み取られた値を格納する場合、 orderof [...]を持つフェンスと同期します。std::memory_order_acquire

refcount の減少は読み取り-変更-書き込み操作であるため、ここで適用する必要があります。

詳しく説明すると、確実にする必要がある操作の順序は次のとおりです。

  1. refcount を値 > 1 に減らす
  2. refcount を 1 に減らす
  3. オブジェクトの削除

2.3.同じスレッドで発生するため、暗黙的に同期されます。どちらも同じ値に対するアトミックな読み取り-変更-書き込み操作であるため、同期されます1.2.これら 2 つが競合する可能性がある場合、最初に参照カウント全体が壊れてしまいます。したがって、残っているのは同期1.3..

これはまさにフェンスが行うことです。からの書き込みは、先ほど説明したように、と同期1.する操作であり、同じ値の読み取りです。と同じスレッド上のフェンスである は、仕様で保証されているように、からの書き込みと同期するようになりました。これは、オブジェクトへの追加の書き込みを必要とせずに発生します (コメントで @KerrekSB によって提案されているように)。これも機能しますが、追加の書き込みのために効率が低下する可能性があります。release2.3.acquire2.1.acquire

結論: メモリの順序付けをいじらないでください。専門家でさえ、それらを誤解しており、パフォーマンスへの影響はほとんどありません。したがって、プロファイリングの実行でパフォーマンスが低下し、これを絶対に積極的最適化する必要があることが証明されていない限り、それらが存在しないふりをしてデフォルトに固執してmemory_order_seq_cstください。

于 2014-06-27T08:27:24.413 に答える