std::shared_ptr 复制构造函数线程安全
std::shared_ptr copy constructor thread safety
std::shared_ptr 规范保证只有一个线程会在内部指针上调用 delete。
这个 对 shared_ptr 引用计数操作所需的内存排序有一个非常好的解释,以保证删除将在同步内存上调用。
我不明白的是:
- 如果一个shared_ptr被拷贝构造函数初始化,是不是
保证它将为空或有效 shared_ptr?
我正在查看 shared_ptr 复制构造函数的 MVCC 实现。我想我至少可以确定一种竞争条件。
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
实施检查控制块是否有效,然后增加其引用计数,并复制分配内部字段。
假设 _Other
属于另一个线程,然后是调用复制构造函数的线程。如果在 if (_Other._Rep)
和 _Other._Rep->_Incref();
行之间,此线程调用恰好删除控制块和指针的析构函数,则 _Other._Rep->_Incref()
将取消引用已删除的指针。
进一步说明
这是一个代码,说明了我正在谈论的极端情况。
我将调整 share_ptr 复制构造函数实现来模拟上下文切换:
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
// now lets put here a really long loop or sleep to simulate a context switch
int count = 0;
for (int i = 0; i < 99999999; ++i)
{
for (int j = 0; j < 99999999; ++j)
{
count++;
}
}
// by the time we get here, the owning thread may already destroy the shared_ptr that was passed to this constructor
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
这是一个可能会显示问题的代码:
int main()
{
{
std::shared_ptr<int> sh1 = std::make_shared<int>(123);
auto lambda = [&]()
{
auto sh2 = sh1;
std::cout << sh2.use_count(); // this prints garbage, -572662306 in my case
};
std::thread t1(lambda);
t1.detach();
// main thread destroys the shared_ptr
// background thread probably did not yet finished executing the copy constructor
}
Sleep(10000);
}
如果shared_ptr
使用得当,你描述的事情就永远不会发生。
被复制的 shared_ptr
在被传递给复制构造函数之前增加了引用计数,并且在复制构造函数退出之前它不能被破坏,因为它是构造函数。
因此,另一个线程不会销毁正在共享的对象。如果 _Other.Rep
不为空,则 _Other.Rep
的引用计数在进入复制构造函数时将始终至少为 1。
更新:您的用例有问题。 lambda 捕获了一个 reference 到主线程的 shared_ptr
实例,但是线程不会复制那个 shared_ptr
直到它已经超出范围并且被 main
摧毁。您的线程有一个 悬空引用 导致您的代码具有 未定义的行为 。这不是 shared_ptr
实现的错误。您的 lambda 需要通过值 而不是 通过引用 来捕获 shared_ptr
,因此它的引用计数会在线程运行之前立即递增创建,而不是在线程启动时 运行.
对 shared_ptr
个对象共享的状态的操作是线程安全的; shared_ptr
本身 不是线程安全的。您不能同时从不同的线程操作同一个 shared_ptr
对象;尝试这样做是一场数据竞赛,因此是 UB。
因此,如果 lambda
在将指针发送到另一个线程之前复制指针,那么您的代码会很好。
还应注意,无论 shared_ptr
如何编写,您的具体示例 永远不会 工作。类型可能是 atomic<int>
并且它仍然会损坏。您为 lambda 提供了一个对象的引用,该对象在 lambda 开始执行复制操作之前可能不存在。
再多的内部线程安全也救不了你。将对堆栈变量的引用传递给另一个线程应该始终被视为代码异味。
std::shared_ptr 规范保证只有一个线程会在内部指针上调用 delete。
这个
我不明白的是:
- 如果一个shared_ptr被拷贝构造函数初始化,是不是 保证它将为空或有效 shared_ptr?
我正在查看 shared_ptr 复制构造函数的 MVCC 实现。我想我至少可以确定一种竞争条件。
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
实施检查控制块是否有效,然后增加其引用计数,并复制分配内部字段。
假设 _Other
属于另一个线程,然后是调用复制构造函数的线程。如果在 if (_Other._Rep)
和 _Other._Rep->_Incref();
行之间,此线程调用恰好删除控制块和指针的析构函数,则 _Other._Rep->_Incref()
将取消引用已删除的指针。
进一步说明
这是一个代码,说明了我正在谈论的极端情况。 我将调整 share_ptr 复制构造函数实现来模拟上下文切换:
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
// now lets put here a really long loop or sleep to simulate a context switch
int count = 0;
for (int i = 0; i < 99999999; ++i)
{
for (int j = 0; j < 99999999; ++j)
{
count++;
}
}
// by the time we get here, the owning thread may already destroy the shared_ptr that was passed to this constructor
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
这是一个可能会显示问题的代码:
int main()
{
{
std::shared_ptr<int> sh1 = std::make_shared<int>(123);
auto lambda = [&]()
{
auto sh2 = sh1;
std::cout << sh2.use_count(); // this prints garbage, -572662306 in my case
};
std::thread t1(lambda);
t1.detach();
// main thread destroys the shared_ptr
// background thread probably did not yet finished executing the copy constructor
}
Sleep(10000);
}
如果shared_ptr
使用得当,你描述的事情就永远不会发生。
被复制的 shared_ptr
在被传递给复制构造函数之前增加了引用计数,并且在复制构造函数退出之前它不能被破坏,因为它是构造函数。
因此,另一个线程不会销毁正在共享的对象。如果 _Other.Rep
不为空,则 _Other.Rep
的引用计数在进入复制构造函数时将始终至少为 1。
更新:您的用例有问题。 lambda 捕获了一个 reference 到主线程的 shared_ptr
实例,但是线程不会复制那个 shared_ptr
直到它已经超出范围并且被 main
摧毁。您的线程有一个 悬空引用 导致您的代码具有 未定义的行为 。这不是 shared_ptr
实现的错误。您的 lambda 需要通过值 而不是 通过引用 来捕获 shared_ptr
,因此它的引用计数会在线程运行之前立即递增创建,而不是在线程启动时 运行.
对 shared_ptr
个对象共享的状态的操作是线程安全的; shared_ptr
本身 不是线程安全的。您不能同时从不同的线程操作同一个 shared_ptr
对象;尝试这样做是一场数据竞赛,因此是 UB。
因此,如果 lambda
在将指针发送到另一个线程之前复制指针,那么您的代码会很好。
还应注意,无论 shared_ptr
如何编写,您的具体示例 永远不会 工作。类型可能是 atomic<int>
并且它仍然会损坏。您为 lambda 提供了一个对象的引用,该对象在 lambda 开始执行复制操作之前可能不存在。
再多的内部线程安全也救不了你。将对堆栈变量的引用传递给另一个线程应该始终被视为代码异味。