You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

LLVM17下clang++双向weak_ptr引发TSAN数据竞争问题咨询

多线程下shared_ptr/weak_ptr双向引用的TSAN数据竞争问题

我使用搭配LLVM17的clang++,现有Foo和Bar两个类,二者通过weak_ptr成员变量双向引用。多线程场景下,线程获取shared_ptr<Bar>副本后销毁时,TSAN会报告数据竞争。

示例代码

class Bar;
class Foo : public std::enable_shared_from_this<Foo> {
public:
    std::shared_ptr<Bar> get_instance() {
        std::lock_guard<std::mutex> lck(m_);

        std::shared_ptr<Bar> sh = _ptr.lock();
        if (sh) {
            return sh;
        } else {
            sh = std::make_shared<Bar>(weak_from_this());
            _ptr = sh; // <<<<< TSAN关联位置
        }
        return sh;
    }

private:
    std::weak_ptr<Bar> _ptr;
    std::mutex m_;
};

class Bar {
public:
    Bar(std::weak_ptr<Foo> ptr) : _ctx(ptr) {}
private:
    std::weak_ptr<Foo> _ctx;
};

void get_and_release(std::shared_ptr<Foo> aP) {
    for (int i=0; i<1000; i++) {
        std::shared_ptr<Bar> p = aP->get_instance(); // <<<<< 销毁时触发竞争
    }
}

int main() {
    std::shared_ptr<Foo> aP = std::make_shared<Foo>();
    std::vector<std::thread> threads;
    for (int t=0; t<4; t++) {
        threads.emplace_back(get_and_release, aP);
    }
    for (auto& thread : threads) {
        thread.join();
    }
}

TSAN报错信息

WARNING: ThreadSanitizer: data race (pid=21947)
  Write of size 8 at 0x7b0c00001800 by thread T2 (mutexes: write M0):
    #0 operator delete(void*) ../lib/tsan/rtl/tsan_new_delete.cpp:126:3 (example+0xea98e)
    #1 void std::__1::__libcpp_operator_delete[abi:ue170006]<void*>(void*) ../llvm-17.0.6-n6964152/include/c++/v1/new:278:3 (example+0xed245)
    #2 void std::__1::__do_deallocate_handle_size[abi:ue170006]<>(void*, unsigned long) ../llvm-17.0.6-n6964152/include/c++/v1/new:302:10 (example+0xed1c1)
    #3 std::__1::__libcpp_deallocate[abi:ue170006](void*, unsigned long, unsigned long) ../llvm-17.0.6-n6964152/include/c++/v1/new:318:14 (example+0xed0da)
    #4 std::__1::allocator<std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> > >::deallocate[abi:ue170006](std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> >*, unsigned long) ../llvm-17.0.6-n6964152/include/c++/v1/__memory/allocator.h:130:13 (example+0xed03e)
    #5 std::__1::allocator_traits<std::__1::allocator<std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> > > >::deallocate[abi:ue170006](std::__1::allocator<std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> > >&, std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> >*, unsigned long) ../llvm-17.0.6-n6964152/include/c++/v1/__memory/allocator_traits.h:288:13 (example+0xecfc5)
    #6 std::__1::__shared_ptr_emplace<Bar, std::__1::allocator<Bar> >::__on_zero_shared_weak() ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:332:9 (example+0xecc7c)
    #7 std::__1::weak_ptr<Bar>::~weak_ptr() ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:1777:19 (example+0xed786)
    #8 std::__1::enable_if<__compatible_with<Bar, Bar>::value, std::__1::weak_ptr<Bar>&>::type std::__1::weak_ptr<Bar>::operator=<Bar>(std::__1::shared_ptr<Bar> const&) ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:1836:5 (example+0xebf37)
    #9 Foo::get_instance() A.cc:18:18 (example+0xeb5c7)
    #10 get_and_release(std::__1::shared_ptr<Foo>) A.cc:37:38 (example+0xeb305)

  Previous read of size 8 at 0x7b0c00001800 by thread T1:
    #0 std::__1::__shared_count::__release_shared[abi:ue170006]() ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:172:9 (example+0xed8f0)
    #1 std::__1::__shared_weak_count::__release_shared[abi:ue170006]() ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:213:27 (example+0xed889)
    #2 std::__1::shared_ptr<Bar>::~shared_ptr[abi:ue170006]() ../llvm-17.0.6-n6964152/include/c++/v1/__memory/shared_ptr.h:772:23 (example+0xeb6d6)
    #3 get_and_release(std::__1::shared_ptr<Foo>) A.cc:38:5 (example+0xeb313)

我原以为每个线程持有智能指针副本就应该安全,哪里存在理解误区?另外我曾尝试将Bar中的std::weak_ptr<Foo> _ctx改为std::shared_ptr<Foo>,但只是降低了竞争概率,并未彻底解决问题。


问题分析与解决

核心误区

你忽略了weak_ptr的析构(包括赋值时旧对象的析构)可能触发控制块内存释放,而这个释放操作与其他线程销毁shared_ptr时的控制块读取操作没有同步。

具体来说:

  1. shared_ptr和weak_ptr共享同一个控制块,里面存着强引用计数(shared_count)和弱引用计数(weak_count);
  2. 当最后一个shared_ptr销毁时,shared_count降为0,Bar对象会被销毁,但只要还有weak_ptr指向控制块,weak_count不为0,控制块不会释放;
  3. 当最后一个weak_ptr销毁时(比如代码中_ptr = sh会销毁旧的weak_ptr),weak_count降为0,控制块会被释放;
  4. 如果此时有线程正在销毁shared_ptr,它会读取控制块中的引用计数,而另一个线程已经开始释放控制块内存,这就造成了对同一块内存的读写竞争——TSAN报的正是这个场景。

改成shared_ptr只是延长了Foo的生命周期,让Bar的控制块被weak_ptr引用的时间更久,降低了竞争发生的概率,但本质的并发冲突场景并没有消失。

解决方法

方案1:让Foo持有Bar的强引用

把Foo中的std::weak_ptr<Bar> _ptr改为std::shared_ptr<Bar> _ptr,让Foo始终持有Bar的强引用,这样Bar的控制块的shared_count永远不会降为0,也就不会触发控制块的释放操作:

class Foo : public std::enable_shared_from_this<Foo> {
public:
    std::shared_ptr<Bar> get_instance() {
        std::lock_guard<std::mutex> lck(m_);

        if (_ptr) {
            return _ptr;
        } else {
            _ptr = std::make_shared<Bar>(weak_from_this());
        }
        return _ptr;
    }

private:
    std::shared_ptr<Bar> _ptr;
    std::mutex m_;
};

这种方案彻底消除了控制块被释放的场景,所有shared_ptr的销毁操作只是原子性地递减强引用计数,不会引发内存竞争。

方案2:同步weak_ptr析构与shared_ptr操作

如果必须保留weak_ptr,可以在修改_ptr前,先将旧的weak_ptr提升为shared_ptr,确保控制块的强引用计数至少为1,避免在修改过程中控制块被释放:

std::shared_ptr<Bar> get_instance() {
    std::lock_guard<std::mutex> lck(m_);

    std::shared_ptr<Bar> sh = _ptr.lock();
    if (sh) {
        return sh;
    } else {
        // 保留旧的强引用,避免控制块在赋值过程中被释放
        std::shared_ptr<Bar> old_bar = _ptr.lock();
        sh = std::make_shared<Bar>(weak_from_this());
        _ptr = sh;
    }
    return sh;
}

这个方法通过锁的保护,确保旧控制块的强引用计数在weak_ptr析构前不会降为0,避免了释放操作与其他线程的读取操作并发。

内容的提问来源于stack exchange,提问作者Joe M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 14:47:39