互斥锁的指针进行双重NULL检查的原因是什么

时间:2019-06-04 09:21:57

标签: c++ if-statement locking

我最近读了一本关于系统软件的书。 其中有一个我不理解的例子。

val filtered = all.map(_.split(" ").toList)
                  .filter{ case x::_ => keys.contains(x) }
                  .map(_.mkString(" "))

println(filtered) // -> List(0000005 82 79 16 21 80, 0000001 46 39 8 5 21, 0000007 50 2 33 15 62)

作者为什么要两次检查volatile T* pInst = 0; T* GetInstance() { if (pInst == NULL) { lock(); if (pInst == NULL) pInst = new T; unlock(); } return pInst; }

2 个答案:

答案 0 :(得分:22)

当两个线程尝试同时首次调用GetInstance()时,两个线程都将在第一次检查时看到pInst == NULL。一个线程将首先获得该锁,从而允许它修改pInst

第二个线程将等待锁可用。当第一个线程释放锁时,第二个线程将获得该锁,现在第一个线程已经修改了pInst的值,因此第二个线程无需创建新实例。

只有lock()unlock()之间的第二次检查是安全的。无需先检查就可以工作,但是会变慢,因为每次对GetInstance()的调用都会调用lock()unlock()。第一次检查可以避免不必要的lock()调用。

volatile T* pInst = 0;
T* GetInstance()
{
  if (pInst == NULL) // unsafe check to avoid unnecessary and maybe slow lock()
  {
   lock(); // after this, only one thread can access pInst
   if (pInst == NULL) // check again because other thread may have modified it between first check and returning from lock()
     pInst = new T;
   unlock();
  }
  return pInst;
}

另请参见https://en.wikipedia.org/wiki/Double-checked_locking(摘自interjay的评论)。

注意:此实现要求对volatile T* pInst的读写访问都是原子的。否则,第二线程可能会读取仅由第一线程写入的部分写入的值。对于现代处理器,访问指针值(而不是指向数据)是一种原子操作,尽管并非所有架构都可以保证。

如果对pInst的访问不是原子的,则第二个线程在获取锁之前检查pInst时可能会读取部分写入的非NULL值,然后可能在第一个线程之前执行return pInst线程已完成其操作,这将导致返回错误的指针值。

答案 1 :(得分:2)

我认为lock()是昂贵的操作。我还假设在该平台上原子T*的读取是原子完成的,因此您不需要锁定简单的比较pInst == NULL,因为pInst值的加载操作将是ex。该平台上的单个汇编指令。

假定:如果lock()是一项昂贵的操作,则最好不要执行它,如果我们不必这样做的话。因此,我们首先检查是否为pInst == NULL。这将是一条汇编指令,因此我们不需要lock()。如果pInst == NULL,我们需要修改它的值,分配新的pInst = new ...

但是-设想一种情况,在第一个pInst == NULLlock()之前的两个点之间恰好有2个(或更多)线程。两个线程都将到达pInst = new。他们已经检查了第一个pInst == NULL,对他们两个来说都是如此。

第一个(任意)线程开始执行并执行lock(); pInst = new T; unlock()。然后等待lock()的第二个线程开始执行。当它启动时,pInst != NULL,因为另一个线程分配了它。因此,我们需要在pInst == NULL内再次检查lock(),以免内存泄漏和pInst被覆盖。.