互斥锁指针双重空检查的原因是什么?

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

本文关键字:是什么 检查 指针      更新时间:2023-10-16

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

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(摘自Interjay的评论)。

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

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

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

假设:如果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被覆盖。