双检查锁的错误

双检查锁的应用场景:
多线程中 在一个函数中初始化对象并调用对象的方法

使用双检查锁,
是为了保证对象存在的情况下去调用它,同时为保证程序逻辑正确的情况下减小锁的粒度

然后,被后人诟病的
双检查锁的缺点或者说错误是什么?

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++并发编程实战》

猜你喜欢

转载自blog.csdn.net/adlatereturn/article/details/109133084