【问题标题】:ThreadSanitizer reports "data race on operator delete(void*)" when using embedded reference counter使用嵌入式引用计数器时,ThreadSanitizer 报告“操作员删除(void *)上的数据竞争”
【发布时间】:2018-03-10 01:27:21
【问题描述】:

请看下面的代码:

#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 中的Reference counting 示例。

主要区别在于嵌入的ref_count_在构造函数中被初始化为1(一旦构造函数完成,我们就有一个对ReferenceCounted对象的引用)并且代码不使用@987654331 @。

请不要责怪我在代码中使用了delete this - 这是我在大型代码库中使用的模式,我现在对此无能为力。

现在使用来自主干的clang 3.5(详情如下)和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

请注意,在我的机器上,boost::atomic&lt;T&gt; 的实现解析为 __atomic_load_n 系列函数,即 ThreadSanitizer claims to understand

更新 1:使用 clang 3.4 最终版本时也会发生同样的情况。

更新 2:-std=c++11&lt;atomic&gt;libstdc++libc++ 出现相同的问题。

【问题讨论】:

  • 我不知道 Boost 是什么意思,因为 C++ 标准没有说明任何关于 Boost 的内容,但如果这些是 std 原子,那么 std::atomic_thread_fence 只会与其他原子操作同步如果有合适的中间访问。有关详细信息,请参阅 [atomic.fences]。
  • 尝试将栅栏改为ref_count_.fetch_add(0, boost::memory_order_acquire)
  • @KerrekSB,确实,将栅栏更改为您建议的fetch_add() 会使 tsan 安静下来。但是,我不确定这是否只是 tsan 的一种解决方法,或者它实际上修复了代码中的竞争。
  • 在下面查看我的 cmets。围栏按某些规则行事。如果你愿意,你可以推理规则,或者你可以不使用栅栏。 Fences 是一个糟糕的原语,因为您不想在某些特定对象上进行同步是非常不寻常的。
  • 看看 std::atomic 是否报告同样的问题会很有趣。

标签: c++ multithreading boost reference-counting thread-sanitizer


【解决方案1】:

这看起来像是误报。

release() 方法中的thread_fence 强制所有未完成的写入从fetch_sub-调用到 happen-before 栅栏返回。因此,下一行的delete 不能与之前的写入竞争以减少引用计数。

引用C++ Concurrency in Action一书:

释放操作与带有order 的栅栏同步 std::memory_order_acquire [...] 如果该释放操作存储一个 在栅栏之前由原子操作读取的值 和栅栏一样的线。

由于减少引用计数是一个读-修改-写操作,这应该适用于此。

详细来说,我们需要保证的操作顺序如下:

  1. 将引用计数减少到一个值 > 1
  2. 将引用计数减少到 1
  3. 删除对象

2.3. 是隐式同步的,因为它们发生在同一个线程上。 1.2. 是同步的,因为它们都是对相同值的原子读-修改-写操作。如果这两个人可以比赛,那么整个 refcounting 将首先被打破。所以剩下的就是同步1.3.

这正是栅栏的作用。来自1. 的写入是release 操作,正如我们刚刚讨论的,与2. 同步,读取相同的值。 3.,与2. 在同一线程上的acquire 栅栏,现在与规范所保证的1. 的写入同步。无需在对象上添加 acquire 写入即可发生这种情况(正如 cmets 中的 @KerrekSB 所建议的那样),这也可以工作,但由于额外的写入,效率可能会降低。

底线:不要玩弄内存排序。即使是专家也会弄错它们,它们对性能的影响通常可以忽略不计。因此,除非您在分析运行中证明它们会影响您的性能并且您绝对必须对此进行优化,否则请假装它们不存在并坚持使用默认的 memory_order_seq_cst

【讨论】:

  • 我对这个答案持怀疑态度。 TSan 非常好,实际上非常准确地运行了 C++ 内存模型。见我上面的评论;我认为OP的代码确实不同步。
  • @KerrekSB 查看我的编辑。 boost:atomic 是supposed to be 100% compliant with the standard atomics。像往常一样使用原子,所有这些都是非常微妙的,所以我不排除我在某个地方解释错了。
  • 好的,谢谢。请参阅引用的标准参考 - 29.8/4 听起来像你需要,在栅栏之前,读取正在释放的值的操作,否则栅栏不会与释放同步。
  • @KerrekSB 我猜在标准的那个部分中,X 将是执行删除的线程的写入,A 将是另一个线程的写入(仅减少引用计数,但不删除)。所以A 应该与栅栏同步(在这种情况下,栅栏与X 在同一线程上运行),这是消除比赛所必需的。
  • 这实际上是误报 - it has been confirmed 在线程清理器讨论组中。
【解决方案2】:

仅在撰写本文时(2018 年 3 月)ThreadSanitizer does not support standalone memory fences 为其他偶然发现此问题的人强调 @adam-romanek's commentThreadSanitizer FAQ 中提到了这一点,没有明确提到支持栅栏:

问:支持哪些同步原语? TSan 支持 pthread 同步原语,内置编译器原子操作(同步/原子),llvm libc++ 支持 C++ 操作(不是很彻底 [原文如此]不过经过测试)。

【讨论】:

    猜你喜欢
    • 2022-08-20
    • 2016-07-07
    • 1970-01-01
    • 1970-01-01
    • 2011-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-04
    相关资源
    最近更新 更多