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_); <strong> if (!pInstance_) </strong> // 一定要在“锁”之内 { 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处理器的平行特性。请优先查阅编译器说明文档选择是否使用该方法。