shared_ptr线程不安全程度探究:标准是否强制内存顺序保证?
我希望有人能帮我验证对C++标准的理解:
C++标准是否强制要求:shared_ptr的析构函数必须保证,对共享对象的访问操作happens-before其他线程中该对象的析构函数调用?
背景与代码示例
众所周知,shared_ptr仅通过引用计数管理对象生命周期,其管理的共享对象本身并非线程安全——共享对象仍通过普通指针访问,shared_ptr无法提供额外的线程安全保护。
我原本认为以下代码不存在数据竞争:
#include <memory> #include <semaphore> #include <thread> void foo(std::shared_ptr<int>* in, std::binary_semaphore* flag, int* out) { int temp; { std::shared_ptr<int> in_copy = *in; // 拷贝shared_ptr flag->release(); // 通知主线程拷贝完成 temp = *in_copy; // 读取共享对象的值 // in_copy 在此处析构 } *out = temp; // 将读取的值写入out } int bar() { int out_value; std::binary_semaphore flag{ 0 }; std::thread t1; { // 创建shared_ptr并传递给线程 std::shared_ptr<int> in_value = std::make_shared<int>(15); t1 = std::thread{ &foo, &in_value, &flag, &out_value }; flag.acquire(); // 等待线程完成shared_ptr的拷贝 // in_value 在此处析构 } t1.join(); // 等待线程执行完毕 return out_value; }
潜在的数据竞争担忧
我特别担心*out = temp;会引发数据竞争——如果编译器允许将temp = *in_copy;的读取操作重排至shared_ptr析构之后,代码逻辑会等价于:
void foo(std::shared_ptr<int>* in, std::binary_semaphore* flag, int* out) { // 拷贝shared_ptr的底层逻辑 __refcount* in_refcount = in->get_refcount(); in_refcount->increment(); int* in_copy = in->get(); flag->release(); // 通知主线程拷贝完成 if (in_refcount->decrement_and_check_zero()) { *out = *in_copy; destroy in_copy; } else { *out = *in_copy; } }
此时else分支存在明显的数据竞争:在in_refcount->decrement_and_check_zero()与*in_copy的读取操作之间,其他线程可能已经销毁了共享对象,导致读取已结束生命周期的对象。
主流实现的处理方式
目前三大主流标准库实现(GCC libstdc++、Clang libc++、MSVC STL)均不允许这种重排——它们将引用计数的递减操作设为release语义的原子操作,在共享对象的读取操作与引用计数递减之间建立了happens-before关系,从而确保共享对象的访问与析构操作在多线程间的顺序性。
标准条款的疑问
但关键问题在于:C++标准是否强制要求这种内存顺序保证?我查阅标准后发现相关描述存在明显空白:
- [util.smartptr.shared]仅说明
shared_ptr实现共享所有权语义,use_count()的修改不会引入数据竞争; - [util.smartptr.shared.dest]仅说明
shared_ptr销毁后,其他共享实例的use_count()会减1。
这些描述仅赋予引用计数类似宽松原子操作的语义,但并未明确其为原子操作,因此无法依赖标准中基于原子操作的线程间顺序规则;同时shared_ptr也未像mutex::lock()/unlock()那样被明确列为具备同步语义的操作。
最终困惑
综上,我未在标准中找到任何条款,能够保证shared_ptr所管理对象的访问操作,与其他线程中该对象的析构函数调用之间存在happens-before关系。这意味着使用shared_ptr时,每次其出作用域都需要额外的同步操作(如内存屏障)来避免对象访问与析构间的数据竞争——这显然不合理,会让shared_ptr在多数场景下几乎无用,但我始终无法找到标准中强制要求该内存顺序保证的依据,希望有人指出我的理解错误。
内容的提问来源于stack exchange,提问作者user19232978

