我最近读了一本关于系统软件的书。 其中有一个我不理解的例子。
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;
}
?
答案 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 == NULL
和lock()
之前的两个点之间恰好有2个(或更多)线程。两个线程都将到达pInst = new
。他们已经检查了第一个pInst == NULL
,对他们两个来说都是如此。
第一个(任意)线程开始执行并执行lock(); pInst = new T; unlock()
。然后等待lock()
的第二个线程开始执行。当它启动时,pInst != NULL
,因为另一个线程分配了它。因此,我们需要在pInst == NULL
内再次检查lock()
,以免内存泄漏和pInst
被覆盖。.