双检查锁的应用场景:
多线程中 在一个函数中初始化对象并调用对象的方法
使用双检查锁,
是为了保证对象存在的情况下去调用它,同时为保证程序逻辑正确的情况下减小锁的粒度
然后,被后人诟病的
双检查锁的缺点或者说错误是什么?
void undefined_behaviour_with_double_checked_locking()
{
if(!resource_ptr) // 1
{
std::lock_guard<std::mutex> lk(resource_mutex);
if(!resource_ptr) // 2
{
resource_ptr.reset(new some_resource); // 3
}
}
resource_ptr->do_something(); // 4
}
如我们所见,第一次检查resource_ptr是否拥有数据,上锁后,再检查一次resource_ptr是否拥有数据,为resource_ptr内构造新数据,之后才在使用含有新数据的智能指针去进行某种操作。
这里的1,3处存在潜在的条件竞争,在3处先是构造了一个some_resource对象,接着我们深入reset源码,
template<typename _Yp>
_SafeConv<_Yp>
reset(_Yp* __p) // _Yp must be complete.
{
// Catch self-reset errors.
__glibcxx_assert(__p == 0 || __p != _M_ptr);
__shared_ptr(__p).swap(*this);
}
用了一个匿名的对象去构造__shared_ptr和本身__shared_ptr进行swap交换,
void
swap(__shared_ptr<_Tp, _Lp>& __other) noexcept
{
std::swap(_M_ptr, __other._M_ptr);
_M_refcount._M_swap(__other._M_refcount);
}
这里是先交换了指针,再交换了引用计数,理论上说在这一步交换指针完成的时候resource_ptr就已经满足resource_ptr!=nullptr,另一个线程一看,就直接去执行 resource_ptr->do_something()了。然而在new这个过程,可能会出现cpu乱序问题,也就是先分配了地址再去执行构造操作,导致resource_ptr实际上获得到的是一个没有构造完的指针,是的,此时另一个线程看到的是一个错误的指针,不但这个指针内部调用的函数可能出错,还可能访问到错误的(未初始化的)数据。
为了提高性能而导致了程序的不确定性,这真的令人感到遗憾。
参考自《c++并发编程实战》