对互斥锁的指针进行双重 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 == NULL
和 lock()
之前的点之间。两个线程都将 pInst = new
。他们已经检查了第一个 pInst == NULL
并且对他们两个都是正确的。
第一个(任何)线程开始执行并执行 lock(); pInst = new T; unlock()
。然后等待 lock()
的第二个线程开始执行。当它开始时,pInst != NULL
,因为另一个线程分配了它。所以我们需要在lock()
里面再检查一下pInst == NULL
,这样内存才不会泄露pInst
被覆盖..
最近看了一本关于系统软件的书。 里面有个例子我看不懂
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 == NULL
和 lock()
之前的点之间。两个线程都将 pInst = new
。他们已经检查了第一个 pInst == NULL
并且对他们两个都是正确的。
第一个(任何)线程开始执行并执行 lock(); pInst = new T; unlock()
。然后等待 lock()
的第二个线程开始执行。当它开始时,pInst != NULL
,因为另一个线程分配了它。所以我们需要在lock()
里面再检查一下pInst == NULL
,这样内存才不会泄露pInst
被覆盖..