void undefined_behaviour_with_double_checked_locking()
{
if(!resource_ptr) #1
{
std::lock_guard<std::mutex> lk(resource_mutex); #2
if(!resource_ptr) #3
{
resource_ptr.reset(new some_resource); #4
}
}
resource_ptr->do_something(); #5
}
如果一个线程看到另一个线程写的指针,它可能不会 看到新创建的some_resource实例,导致调用 do_something()操作不正确的值。这是一个例子 由C ++标准定义为数据竞争的竞争条件类型, 因此被指定为未定义的行为。
问题&GT;我已经看到上面的解释为什么代码具有导致竞争条件的双重检查锁定问题。但是,我仍然很难理解问题所在。也许一个具体的双线程逐步工作流程可以帮助我真正理解上面代码的竞争问题。
本书提到的解决方案之一如下:
std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;
void init_resource()
{
resource_ptr.reset(new some_resource);
}
void foo()
{
std::call_once(resource_flag,init_resource); #1
resource_ptr->do_something();
}
#1 This initialization is called exactly once
欢迎任何评论 - 谢谢你
答案 0 :(得分:8)
在这种情况下(取决于.reset
和!
的实现),当线程1初始化resource_ptr
然后暂停/切换时,可能会出现问题。然后线程2出现,执行第一次检查,发现指针不为空,并跳过锁定/完全初始化的检查。然后它使用部分初始化的对象(可能导致不好的事情发生)。线程1然后返回并完成初始化,但为时已晚。
部分初始化resource_ptr
可能的原因是因为允许CPU重新排序指令(只要它不改变单线程行为)。因此,虽然代码看起来应该完全初始化对象然后将其分配给resource_ptr
,但优化的汇编代码可能会做一些完全不同的事情,并且CPU也不能保证在运行汇编指令命令它们在二进制文件中指定!
需要注意的是,当涉及多个线程时,内存栅栏(锁)是保证事情按正确顺序发生的唯一方法。
答案 1 :(得分:4)
最简单的问题场景是some_resource
的初始化不依赖于resource_ptr
。在这种情况下,编译器可以在完全构造resource_ptr
之前为some_resource
分配值。
例如,如果您认为new some_resource
的操作包含两个步骤:
some_resource
some_resource
(对于这个讨论,我将简化假设这个初始化不能抛出异常)然后你可以看到编译器可以实现代码互斥的代码:
1. allocate memory for `some_resource`
2. store the pointer to the allocated memory in `resource_ptr`
3. initialize `some_resource`
现在很明显,如果另一个线程在步骤2和3之间执行该功能,那么resource_ptr->do_something()
可以在some_resource
尚未初始化时调用。
请注意,在某些处理器体系结构中,这种重新排序也可能在硬件中发生,除非存在适当的内存屏障(并且这些屏障将由互斥锁实现)。