解释双重检查锁定中的竞争条件

发布于 2024-12-21 22:30:51 字数 1077 浏览 3 评论 0原文

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

欢迎任何评论 -谢谢

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
}

if a thread sees the pointer written by another thread, it might not
see the newly-created instance of some_resource, resulting in the call
to do_something() operating on incorrect values. This is an example of
the type of race condition defined as a data race by the C++ Standard,
and thus specified as undefined behaviour.

Question> I have seen the above explanation for why the code has the double checked locking problem that causes the race condition. However, I still have difficulties to understand what the problem is. Maybe a concrete two-threads step-by-step workflow can help me really understand the race problem for the above the code.

One of the solution mentioned by the book is as follows:

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

Any comment is welcome
-Thank you

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

寂寞陪衬 2024-12-28 22:30:51

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

部分初始化的 resource_ptr 之所以可能,是因为允许 CPU 重新排序指令(只要它不改变单线程行为)。因此,虽然代码看起来应该完全初始化对象,然后将其分配给 resource_ptr,但优化后的汇编代码可能会执行完全不同的操作,并且 CPU 也不能保证运行汇编指令按照二进制中指定的顺序排列!

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

In this case (depending on the implementation of .reset and !) there may be a problem when Thread 1 gets part-way through initializing resource_ptr and then gets paused/switched. Thread 2 then comes along, performs the first check, sees that the pointer is not null, and skips the lock/fully-initialized check. It then uses the partially-initialized object (probably resulting in bad things happening). Thread 1 then comes back and finishes initializing, but it's too late.

The reason that a partially-initialized resource_ptr is possible is because the CPU is allowed to reorder instructions (as long as it doesn't change single-thread behaviour). So, while the code looks like it should fully-initialize the object and then assign it to resource_ptr, the optimized assembly code might be doing something quite different, and the CPU is also not guaranteed to run the assembly instructions in the order they are specified in the binary!

The takeaway is that when multiple threads are involved, memory fences (locks) are the only way guarantee that things happen in the right order.

那些过往 2024-12-28 22:30:51

最简单的问题场景是 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,那么在 some_resource 尚未初始化时可以调用 resource_ptr->do_something()

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

The simplest problem scenario is in the case where the intialization of some_resource doesn't depend on resource_ptr. In that case, the compiler is free to assign a value to resource_ptr before it fully constructs some_resource.

For example, if you think of the operation of new some_resource as consisting of two steps:

  • allocate the memory for some_resource
  • initialize some_resource (for this discussion, I'm going to make the simplifying assumption that this initialization can't throw an exception)

Then you can see that the compiler could implement the mutex-protected section of code as:

1. allocate memory for `some_resource`
2. store the pointer to the allocated memory in `resource_ptr`
3. initialize `some_resource`

Now it becomes clear that if another thread executes the function between steps 2 and 3, then resource_ptr->do_something() could be called while some_resource has not been initialized.

Note that it's also possible on some processor architectures for this kind of reordering to occur in hardware unless the proper memory barriers are in place (and such barriers would be implemented by the mutex).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文