线程安全性疑问:并发场景下shared_ptr与计数器访问问题
作为并发编程新手,你的判断完全正确——这段代码确实存在多处线程安全问题,执行结果是未定义的。下面我逐个解答你的疑问:
疑问1:Middle::inner_data中对data_->counter的访问是否安全?
完全不安全,存在两层数据竞争:
- 第一层是对
std::shared_ptr<Inner> data_的访问:Middle的后台线程在频繁调用data_.reset(new Inner)(写操作),而main线程通过inner_data()读取data_指向的对象(读操作),这两个操作没有任何同步机制,属于对同一个shared_ptr实例的并发读写,违反了shared_ptr的线程安全规则。 - 第二层是对
counter_的访问:Inner的后台线程在持续自增counter_(写操作),main线程在读取这个值(读操作),std::uint64_t的读写在部分平台上并非原子操作,而且两者之间没有互斥锁、条件变量或内存屏障,会导致数据撕裂或读取到不一致的值,属于典型的数据竞争,触发未定义行为。
首先明确shared_ptr的线程安全边界:
- 多个线程对不同的
shared_ptr实例进行任意操作(包括读写)都是安全的; - 多个线程对同一个
shared_ptr实例的读操作(比如复制构造、赋值、获取引用计数)是安全的; - 但如果对同一个
shared_ptr实例同时进行写操作(比如reset、赋值新对象)和读/写操作,则完全不安全,属于数据竞争。
回到你的场景:线程A对sp执行reset(写操作),线程B同时复制sp(读操作),这是并发读写同一个shared_ptr实例,属于未定义行为。
关于复制失败的风险:确实存在。虽然shared_ptr的引用计数操作是原子的,但shared_ptr内部的指针和引用计数是两个独立的成员,并发读写时可能出现“指针已经指向被销毁的对象,但引用计数还没来得及更新”的情况,导致线程B复制出的shared_ptr指向一个已经被销毁的对象,后续访问这个对象会触发未定义行为(比如崩溃、读取垃圾值)。
疑问3:在何种情况下,未定义行为可能导致std::terminate?能否通过工具检测此类情况?
可能触发std::terminate的场景
未定义行为的结果是不可预测的,但在你的代码中,以下情况可能导致std::terminate:
- 线程异常未捕获:如果Inner或Middle的后台线程在执行过程中抛出未捕获的异常,会直接触发
std::terminate; - 析构函数抛出异常:如果Inner的析构函数中
join线程时发生异常(比如线程已经被 detach 或处于无效状态),而析构函数本身没有捕获异常,会导致std::terminate; - 非法内存访问导致的崩溃:当main线程访问已经被销毁的
Inner对象的counter_时,可能触发段错误,部分操作系统会将这种错误转化为异常,最终导致std::terminate; - double free:如果
shared_ptr的并发读写导致引用计数计算错误,可能出现同一个对象被多次释放的情况,触发内存错误,进而导致std::terminate。
检测工具
这类并发问题可以通过以下工具检测:
- 静态分析工具:Clang-Tidy、Cppcheck,能在编译阶段发现潜在的线程安全问题(比如未同步的共享变量访问);
- 动态分析工具:
- ThreadSanitizer(TSAN):专门检测数据竞争,能精准定位到代码中并发读写的位置;
- Valgrind的Helgrind/DRD:检测线程同步错误和数据竞争;
- AddressSanitizer(ASAN):检测非法内存访问、double free等内存错误,常和TSAN配合使用。
你的示例程序运行多日未出现异常,只是未定义行为的“幸运”情况——未定义行为不代表一定会崩溃,只是结果不可靠,在生产环境中可能会在极端负载或特定平台下突然爆发问题,必须修复。
内容的提问来源于stack exchange,提问作者arynaq
相关产品推荐
相关产品推荐

