Singleton Dead Reference 问题
假设有程序使用了3个 Singletons:Keyboard、Display 和 Log。前两者分别模拟所对应的真实物体,Log 用于错误报告(可以是一个文本文件或者控制台显示等)。程序 Log 构造需要一定开销,最好在出现错误时构造,程序执行过程没有任何错误时,Log 根本不会产生。
程序会向 Log 报告 Keyboard 或 Display 构造或摧毁时发生的错误。如果我们以 Meyer singletons 实现上述三者,程序并不正确。举个例子:假设 Keyboard 成功构造之后 Display 初始化失败,于是 Display 的构造函数会产生一个 Log 记录错误,而且程序准备结束。此时,语言规则发挥作用:执行期相关机制会摧毁局部静态对象,摧毁次序和生成相反。因而 Log 会在 Keyboard 之前摧毁(因为在Display 构造时发生错误而生成 Log)。但万一 Keyboard 关闭失败并向 Log 报告错误, Log::Instance() 会不明就理的回传一个 reference,纸箱一个已被摧毁的 Log 对象的“空壳”。于是程序步入了 “行为不确定”的情况。这就是 Dead Reference 问题。
怎么检测 Dead Reference
class Singleton
{
public:
static Singleton& Instance()
{
if (pInstance_)
{
if (destroyed_)
{
OnDeadReference();
}
else
{
Create();
}
}
return *pInstance_;
}
private:
// Create a new Singleton and store a pointer to it in pInstance_
static void Create()
{
static Singleton theInstance;
pInstance_ = &theInstance;
}
// Gets called if dead reference detected
static void OnDeadReference()
{
throw std::runtime_error("Dead Reference Detected");
}
// When the process end,~Singleton() will be called
virtual ~Singleton()
{
pInstance_ = 0;
destroyed_ = true;
}
static Singleton *pInstance_;
static bool destroyed_;
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
只要程序结束,Singleton 的析构函数就会被调用,于是 pInstance_ 设为 0 并将 destroyer_ 为 true。如果此后又某个寿命更长的对象试图取这个 Singleton,执行流程会到达OnDeadReference(),于是跑出一个型别为 runtime_error 的异常。
解决 Dead Reference 问题
1 Phoenix Singleton
带有静态变量的 Phoenix Singleton,其实作手法非常简单。一旦检测到 dead reference,我们便在旧躯壳中给一个新的 Singleton 对象(C++ 保存这种可能,因为静态对象的内存在整个程序声明期间都会保留着)。
class Singleton
{
public:
static Singleton& Instance()
{
if (pInstance_)
{
if (destroyed_)
{
OnDeadReference();
}
else
{
Create();
}
}
return *pInstance_;
}
private:
// Create a new Singleton and store a pointer to it in pInstance_
static void Create()
{
static Singleton theInstance;
pInstance_ = &theInstance;
}
// Gets called if dead reference detected
static void OnDeadReference()
{
new (pInstance_) Singleton;
atexit(KillPhoenixSingleton);
destroyed_ = false;
}
static void KillPhoenixSingleton()
{
pInstance_->~Singleton();
}
// When the process end,~Singleton() will be called
virtual ~Singleton()
{
pInstance_ = 0;
destroyed_ = true;
}
static Singleton *pInstance_;
static bool destroyed_;
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
一个问题:如果没有#define ATEXIT_FIXED,新产生的 Phoenix Singleton 将不会被摧毁,因而造成泄露。例如下面这个测试:
void Bar()
{
cout << "Bar..." << endl;
}
void Foo()
{
cout << "Foo..." << endl;
atexit(Bar);
}
int main()
{
atexit(Foo);
}
这个小程序通过 atexit() 登记 Foo。后者调用 atexit(Bar)。C 和 C++ 标准规格都自相矛盾,Bar 会在 Foo 之前被调用,因为 Bar 较晚才登记;但当 Bar 被登记时,它已经无法做到“先被调用”,因为此时 Foo 已经(正在)被调用。这个问题在不同编译器上轻则出错,重则造成程序崩溃。所以根据你的编译器情况可以先定义 #define ATEXIT_FIXED 来解决这个问题。
2 带寿命的 Singleton
2.1 带寿命的Singleton的约束
我们希望有一种i简单方法来控制各种 Singleton 的寿命。如此就可以赋予 Log “比 Keyboard 和 Display 更长” 的生命来解决问题。对象的寿命越长,就越晚摧毁。那就必须写出这样的代码:
// This is a Singleton class
class SomeSingleton {};
// This is a regular class
class SomeClass {};
SomeClass* pGlobalObject(new SomeClass);
int main()
{
SetLongevity(&SomeSingleton().Instance(), 5);
// Ensure pGlobalObject will be deleted
// after SomeSingleton's instance
SetLongevity(pGlobalObject, 6);
}
SetLongevity() 接受两个参数,一个是 reference,指向任意型别对象,另一个整数值,代表寿命:
// Take a reference to an object allocated with new and the longevity of that object
template <typename T>
void SetLongevity(T* pDynObject, unsinged int longevity);
这个函数保证,和其他所有寿命较短的对象相比,pDynObject 存在的时间比较长。当程序结束时,所有通过 SetLongevity() 登记的对象便会根据寿命长短被依次删除。
对那些“寿命受编译器控制”的对象(例如一般全局对象、static 对象、auto 对象)来说,你无法运用 SetLongevity()。编译器已经自动产生了一些代码来摧毁这些对象,如果你又调用 SetLongevity(),它们就会被摧毁两次(这个程序绝对绝无好处)。SetLongevity 针对的只是“经由 new 分配而得” 的对象。此外,对某个对象调用 SetLongevity(),表示你不会对那个对象调用 delete。
由于 SetLongevity() 必须和 atexit() 相处融合,所以必须认真定义这两个函数间的关系。
class SomeClass {};
int main()
{
// Create an object and assign a longevity to it
SomeClass* pObj1 = new SomeClass;
SetLongevity(pObj1, 5);
// Create a static object whose lifetime follows C++ rules
static SomeClass Obj2;
// Create another object and assign a greater longevity to it
SomeClass* pObj3 = new SomeClass;
SetLongevity(pObj3, 6);
// How will these objects be destroyed?
}
main 之中既定义了 “带寿命的对象”,也定义了“遵循 C++ 规则的对象”。为这个三个对象定义一个合理的析构顺序很困难,因为除了使用 atexit(),我们没有任何方法可以操控那个由执行期机制维护的隐藏性 stack。
仔细分析各个约束条件后,得出以下设计决定:
每一个SetLongevity() 调用动作都产生一个 atexit() 调用动作。
短寿对象的析构行为发生在长寿对象的析构行为之前。
寿命相同的对象,其析构遵循 C++ 规则:后构造者先被摧毁。
这些规则会在先前的范例程序中保证这样的析构次序:*pObj1,Obj2,*pObj3。第一次调用 SetLongevity() 会产生一个 atexit 调用动作,用于 *pObj3 的摧毁,第二个调用动作会相应发出一个 atexit 调用动作,用于 *pObj1 的摧毁。
注意:使用它的法则是:任何对象 A 如果使用了带寿命的对象 B, A的寿命就必须短于 B 的寿命。
2.2 实现带寿命的 Singleton
namespace Private
{
class LifetimeTracker
{
public:
LifetimeTracker(unsigned int x)
: longevity_(x)
{}
virtual ~LifetimeTracker() = 0;
friend inline bool Compare(unsigned int longevity,
const LifetimeTracker *p)
{
return p->longevity_ < longevity;
}
private:
unsigned int longevity_;
};
inline LifetimeTracker::~LifetimeTracker() {}
}
namespace Private
{
// TrackerArray 存储 LifetimeTracker 指针
// 长寿命对象靠近 array 头部,形同寿命的对象则一起插入顺序排列
typedef LifetimeTracker** TrackerArray;
TrackerArray gp_pTrackerArray = NULL;
unsigned int gp_elements = 0;
template <typename T>
void Delete(T* pObj)
{
delete pObj;
}
}
namespace Private
{
static void AtExitFn()
{
assert(gp_elements > 0 && gp_pTrackerArray != 0);
// Pick the element at the top of the stack
LifetimeTracker* pTop = gp_pTrackerArray[gp_elements - 1];
// Remove that object off the stack
// Don't check errors-realloc with less memory can't fail
gp_pTrackerArray = static_cast<TrackerArray>(std::realloc(
gp_pTrackerArray, sizeof(LifetimeTracker*) * --gp_elements));
// Destroy the element
delete pTop;
}
}
namespace Private
{
template <typename T, typename Destroyer>
class ConcreteLifetimeTracker : public LifetimeTracker
{
public:
ConcreteLifetimeTracker(T* pDynObject,
unsigned int longevity, Destroyer destroyer)
: LifetimeTracker(longevity)
, pTracked_(pDynObject)
, destroyer_(destroyer)
{
}
~ConcreteLifetimeTracker()
{
destroyer_(pTracked_);
}
private:
T* pTracked_;
Destroyer destroyer_;
};
}
template <typename T, typename Destroyer>
void SetLongevity(T* pDynObject, unsigned int longevity,
Destroyer d = Private::Delete<T>)
{
using namespace Private;
// 如果 gp_pTrackerArray 为 NULL,则数组进行第一次分配
TrackerArray pNewArray = static_cast<TrackerArray>(
std::realloc(gp_pTrackerArray, sizeof(LifetimeTracker*) * (gp_elements + 1)));
if (!pNewArray)
throw std::bad_alloc();
if (!gp_pTrackerArray)
memset(pNewArray, 0, sizeof(LifetimeTracker*) * (gp_elements + 1));
gp_pTrackerArray = pNewArray;
LifetimeTracker *p = new ConcreteLifetimeTracker<T, Destroyer>(
pDynObject, longevity, d);
TrackerArray pos = gp_pTrackerArray;
if (gp_elements != 0)
{ // 二分查找法查找元素插入位置
pos = std::upper_bound(
gp_pTrackerArray, gp_pTrackerArray + gp_elements, longevity, Compare);
std::copy_backward(pos, gp_pTrackerArray + gp_elements,
gp_pTrackerArray + gp_elements + 1);
}
*pos = p;
++gp_elements;
std::atexit(Private::AtExitFn);
}
class Log
{
public:
static void Create()
{
pInstance_ = new Log;
SetLongevity(pInstance_, longevity_, Private::Delete<Log>);
}
private:
static const unsigned int longevity_ = 2;
static Log* pInstance_; // 仅为声明
};
Log* Log::pInstance_ = NULL; // 这才是定义
class Keyboard
{
// ...
};
int main()
{
Log::Create();
Keyboard *pKeyboard = new Keyboard;
SetLongevity(pKeyboard, 1, Private::Delete<Keyboard>);
}
3 多线程情况下的 Singleton
Singleton& Singleton::Instance()
{
if (!pInstance_) // 1
{
pInstance_ = new Singleton; // 2
}
return *pInstance; // 3
}
一个线程进入 Instance 并检测 if 条件。由于这是第一次访问,所以 pInstance_ 为 null,于是进入 // 2 那一行,准备调用 new 操作法。此时有可能 OS 调度器中断了这个线程,将控制权转给了另一个线程。 第二个线程调用 Singleton::Instance(),并发现 pInstance_ 为 null,因为第一个线程没有机会修改它便被 OS 调度器中断。到目前为止第一个线程只是完成了对 pInstance_ 的测试。现在假设第二个线程完成了对 new 的调用,顺利完成了 pInstance_ 的赋值操作并带走它。但是,当第一个线程再次执行时,它记得它应该执行// 2代码,并因此对 pInstance_ 再次赋值。程序有个两个 Singleton 对象而不是一个,其中一个必定造成了内存泄露。
3.1 一般的解决方法
Singleton& Singleton::Instance()
{
// mutex_ is a mutex object
// Lock manages the mutex
Lock guard(mutex_);
if (!pInstance_) // 一定要在“锁”之内
{
pInstance_ = new Singleton;
}
return *pInstance;
}
3.2 双检测锁定
Singleton& Singleton::Instance()
{
if (!pInstance_) // 1
{
// 2
Guard myGuard(lock_); // 3
if (!pInstance_) // 4
pInstance_ = new Singleton;
}
return *pInstance;
}
假设某个线程的控制流程进入了模糊区(注释第 2 行),此处可能有数个线程同时进入。但同步区则是“同一时刻只会有一个线程进入”。到了注释第 3 行,模糊不存在,指针要么已经完全初始化,要么根本没有被初始化。第一个进入的线程会初始化指针变量,其他所有线程都会在注释第 4 行的检测行动中失败,不会产生任何东西。
双检测锁定在大多数情况下对 Singleton 的访问都具备应有的速度,而构造期间也不存在竞态条件。
破坏双检测锁定的因素:
编译器的 code arranger ,它会重新排列编译器所产生出来的汇编语言指令,使代码能够最佳运用 RISC处理器的平行特性。请优先查阅编译器说明文档选择是否使用该方法。