现在的位置: 首页 > 综合 > 正文

Singleton(二)

2018年03月30日 ⁄ 综合 ⁄ 共 7627字 ⁄ 字号 评论关闭
文章目录

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处理器的平行特性。请优先查阅编译器说明文档选择是否使用该方法。 

抱歉!评论已关闭.