gpt4 book ai didi

c++ - ThreadSanitizer 在使用嵌入式引用计数器时报告 "data race on operator delete(void*)"

转载 作者:塔克拉玛干 更新时间:2023-11-03 00:29:25 25 4
gpt4 key购买 nike

请看下面的代码:

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

这有点类似于 Reference counting来自 Boost.Atomic 的示例.

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

请不要怪我使用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 相同的内存位置在引用计数器上进行原子递减时。

前者write如何解释?它是由 ReferenceCounted 的析构函数执行的一些清理工作吗?类(class)?

这是误报?还是代码有误?

我的设置是:

$ 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<T>解析为 __atomic_load_n函数族,ThreadSanitizer claims to understand .

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

更新 2:-std=c++11 也会出现同样的问题和 <atomic>libstdc++libc++ .

最佳答案

这看起来像是误报。

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

引自 C++ Concurrency in Action 一书:

A release operation synchronizes-with a fence with an order of std::memory_order_acquire [...] if that release operation stores a value that's read by an atomic operation prior to the fence on the same thread as the fence.

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

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

  1. 将引用计数减少到大于 1 的值
  2. 将引用计数减少到 1
  3. 删除对象

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

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

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

关于c++ - ThreadSanitizer 在使用嵌入式引用计数器时报告 "data race on operator delete(void*)",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24446561/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com