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

What is the reason for double NULL check of pointer for mutex lock

最近看了一本关于系统软件的书。 里面有个例子我看不懂

volatile T* pInst = 0;
T* GetInstance()
{
  if (pInst == NULL)
  {
   lock();
   if (pInst == NULL)
     pInst = new T;
   unlock();
  }
  return pInst;
}

为什么作者检查(pInst == NULL)两次?

当两个线程同时第一次尝试调用 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 (copied from interjay 的评论)。

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

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

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

假设:如果 lock() 是一个代价高昂的操作,最好不要执行它,如果我们不需要的话。所以首先我们检查是否 pInst == NULL。这将是一个单一的汇编指令,所以我们不需要 lock() 它。如果pInst == NULL,我们需要修改它的值,分配新的pInst = new ....

但是 - 想象一种情况,其中 2 个(或更多)线程恰好位于第一个 pInst == NULLlock() 之前的点之间。两个线程都将 pInst = new。他们已经检查了第一个 pInst == NULL 并且对他们两个都是正确的。

第一个(任何)线程开始执行并执行 lock(); pInst = new T; unlock()。然后等待 lock() 的第二个线程开始执行。当它开始时,pInst != NULL,因为另一个线程分配了它。所以我们需要在lock()里面再检查一下pInst == NULL,这样内存才不会泄露pInst被覆盖..