解释双重检查锁定中的竞争条件
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在这种情况下(取决于
.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 initializingresource_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 toresource_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.
最简单的问题场景是
some_resource
的初始化不依赖于resource_ptr
的情况。在这种情况下,编译器可以在完全构造some_resource
之前为resource_ptr
赋值。例如,如果您认为
new some_resource
的操作由两个步骤组成:some_resource
分配内存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 onresource_ptr
. In that case, the compiler is free to assign a value toresource_ptr
before it fully constructssome_resource
.For example, if you think of the operation of
new some_resource
as consisting of two steps:some_resource
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:
Now it becomes clear that if another thread executes the function between steps 2 and 3, then
resource_ptr->do_something()
could be called whilesome_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).