这个线程sanitizer警告中的竞争条件在哪里?

8
下面的代码在macOS上使用线程安全检测时会产生警告。我看不出其中竞态条件出现的位置。shared_ptr和weak_ptr的控制块是线程安全的,从std :: queue中推入和弹出元素时都在锁定状态下完成。
#include <future>
#include <memory>
#include <queue>

class Foo {
public:
  Foo() {
    fut = std::async(std::launch::async, [this] {
      while (!shouldStop) {
        std::scoped_lock lock(mut);
        while (!requests.empty()) {
          std::weak_ptr<float> requestData = requests.front();
          requests.pop();
          (void)requestData;
        }
      }
    });
  }

  ~Foo() {
    shouldStop.store(true);
    fut.get();
  }

  void add(const std::weak_ptr<float> subscriber) {
    std::scoped_lock lock(mut);
    requests.push(subscriber);
  }

private:
  std::atomic<bool> shouldStop = false;
  std::future<void> fut;
  std::queue<std::weak_ptr<float>> requests;
  std::mutex mut;
};

int main() {
  Foo foo;

  int numIterations = 100000;

  while (--numIterations) {
    auto subscriber = std::make_shared<float>();

    foo.add(subscriber);
    subscriber.reset();
  }

  std::this_thread::sleep_for(std::chrono::seconds(1));
}

带有堆栈跟踪的警告:

WARNING: ThreadSanitizer: data race (pid=11176)
  Write of size 8 at 0x7b0800000368 by thread T1 (mutexes: write M16):
    #0 operator delete(void*) <null>:1062032 (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x4f225)
    #1 std::__1::__shared_ptr_emplace<float, std::__1::allocator<float> >::__on_zero_shared_weak() new:272 (minimal:x86_64+0x1000113de)
    #2 std::__1::weak_ptr<float>::~weak_ptr() memory:5148 (minimal:x86_64+0x100010762)
    #3 std::__1::weak_ptr<float>::~weak_ptr() memory:5146 (minimal:x86_64+0x100002448)
    #4 Foo::Foo()::'lambda'()::operator()() const minimal_race.cpp:15 (minimal:x86_64+0x10000576e)
    #5 void std::__1::__async_func<Foo::Foo()::'lambda'()>::__execute<>(std::__1::__tuple_indices<>) type_traits:4345 (minimal:x86_64+0x1000052f0)
    #6 std::__1::__async_func<Foo::Foo()::'lambda'()>::operator()() future:2323 (minimal:x86_64+0x100005268)
    #7 std::__1::__async_assoc_state<void, std::__1::__async_func<Foo::Foo()::'lambda'()> >::__execute() future:1040 (minimal:x86_64+0x100005119)
    #8 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void (std::__1::__async_assoc_state<void, std::__1::__async_func<Foo::Foo()::'lambda'()> >::*)(), std::__1::__async_assoc_state<void, std::__1::__async_func<Foo::Foo()::'lambda'()> >*> >(void*) type_traits:4286 (minimal:x86_64+0x10000717c)

  Previous atomic write of size 8 at 0x7b0800000368 by main thread:
    #0 __tsan_atomic64_fetch_add <null>:1062032 (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x24cdd)
    #1 std::__1::shared_ptr<float>::~shared_ptr() memory:3472 (minimal:x86_64+0x1000114d4)
    #2 std::__1::shared_ptr<float>::~shared_ptr() memory:4502 (minimal:x86_64+0x100002488)
    #3 main memory:4639 (minimal:x86_64+0x10000210b)

SUMMARY: ThreadSanitizer: data race new:272 in std::__1::__shared_ptr_emplace<float, std::__1::allocator<float> >::__on_zero_shared_weak()

编辑:我使用以下方式进行编译:

clang++ -std=c++17 -g -fsanitize=thread -o test  minimal_race.cpp

Clang版本:

$ clang++ --version
clang version 7.1.0 (tags/RELEASE_710/final)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /usr/local/opt/llvm@7/bin

我正在使用macOS 10.14.6操作系统


2
没有问题。OP的代码有问题,他们不只是想要改进它。竞争条件是一个非常严重的问题。 - Tas
1
成功在Linux上使用clang和libc++得到了更好的跟踪结果...不知道如何在此处发布,以便您可以查看它,请点击此链接。使用clang++ -std=c++17 -O0 -ggdb -fsanitize=thread -stdlib=libc++ -o x x.cpp编译,使用libstdc++则不会出现此问题。 - xception
添加 -stdlib=libc++ @MarekR - xception
我几乎可以确定,在所有地方使用std::shared_ptr的建议将消除警告,因为对象在推入队列后不会直接被删除。上面的示例非常简单,我已经删除了所有东西,以使其尽可能小(例如,在从队列中弹出weak_ptr后调用lock()获取shared_ptr)。 - tuple_cat
就像我所说的,我确实认为这是一个错误,从我的观察来看,它不应该发生。 - xception
显示剩余12条评论
1个回答

1
我认为这可能是libc++和clang在weak_ptr/shared_ptr实现上的一个bug...将weak_ptr队列更改为shared_ptr可以解决问题,即使是旧版本的clang也可以。
更改如下(第25行):
  void add(const std::shared_ptr<float> subscriber) {

第33行:

  std::queue<std::shared_ptr<float>> requests;

将第42行的while重写为:
  while (--numIterations) {
    auto subscriber = std::make_shared<float>();

    foo.add(move(subscriber));
  }

你能否重现它?使用的是哪个Clang版本? - Marek R
对我来说,这听起来很合理,因为这可以防止对象被销毁。竞争条件似乎发生在shared_ptr的析构函数中。 - JHBonarius
clang++ -v clang版本9.0.0(tags/RELEASE_900/final) 目标:x86_64-pc-linux-gnu 线程模型:posix 已安装目录:/usr/lib/llvm/9/bin 选择的GCC安装:/usr/lib/gcc/x86_64-pc-linux-gnu/9.2.0 候选多库:.;@m64 候选多库:32;@m32 选择的多库:.;@m64 @MarekR - xception
@MarekR 我甚至在问题的评论中发布了自己的回溯,并附上了链接和构建标志。 - xception
1
如果我理解正确,使用不同的线程从weak_ptr/shared_ptr进行交互应该是安全的(至少在OP使用shared_ptr和weak_ptr作为不同对象时,只涉及外壳类而不涉及被指向的数据)。 - xception

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接