4
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

欢迎任何评论 - 谢谢

4

2 回答 2

8

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

部分初始化resource_ptr是可能的原因是因为允许 CPU 重新排序指令(只要它不改变单线程行为)。因此,虽然代码看起来应该完全初始化对象,然后将其分配resource_ptr给二进制!

要点是,当涉及多个线程时,内存栅栏(锁)是保证事情以正确顺序发生的唯一方法。

于 2011-12-20T05:00:14.063 回答
5

最简单的问题场景是初始化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尚未初始化的情况下调用该函数。

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

于 2011-12-20T10:45:21.020 回答