3
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++标准定义为数据争用 ,因此被指定为未定义的行为。解释竞态条件双重检查锁定

问题>我已经看到了为什么代码具有双重检查锁定的问题导致了比赛状态,在上述说明。但是,我仍然很难理解问题所在。也许一个具体的双线程分步工作流程可以帮助我真正理解上述代码的竞争问题。

一个接书中提到的解决方案如下:

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 

任何意见,欢迎 - 谢谢

+1

阅读[这个答案]的第二部分(http://stackoverflow.com/a/367690/14065)的一些非显而易见的问题。 – 2011-12-20 07:03:39

回答

4

最简单的问题场景是在some_resource的初始化不依赖于resource_ptr的情况下。在这种情况下,编译器可以在完全构造some_resource之前自由地将值分配给resource_ptr

例如,如果你想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尚未初始化调用。

请注意,在某些处理器体系结构中,这种重新排序也可能发生在硬件中,除非适当的内存屏障已到位(并且此类屏障将由互斥体实现)。

7

在此情况下(取决于.reset实施和!)有可能当线程1通过初始化resource_ptr中途暂停/切换时,会成为问题。线程2然后出现,执行第一次检查,看到指针不为空,并跳过锁定/完全初始化检查。然后它使用部分初始化的对象(可能导致不好的事情发生)。线程1然后返回并完成初始化,但已经太晚了。

+0

所以你的关键是,如果'.reset'被烧烤并暂停,竞赛问题就会发生。换句话说,如果'.reset'不是一个原子操作,那么问题就会发生。 – q0987 2011-12-20 05:11:08

+0

是的,重置和!操作员是关键。如果你看看这些方法的实现,还要记住编译器可能会为你重新排列一些语句(这是导致重复检查锁定问题的常见原因)。 – jnnnnn 2011-12-20 06:30:03