四、多线程安全的通用单例类实例获取器
例子工程的名称是SingletonThreadSafeInstanceGetter。
刚开始写本文时,本没有想实现多线程版本,主观上以为同通常的单例模式一样,多个多线程同步就可以了,很简单,让读者自己开发就好了,不过后来真正去思考时发现不是那么简单的,感觉对此还是很有介绍的必要。
1、单例类实例与单例类实例获取器实例的对应关系
在实现多线程安全的通用单例类实例获取器前,将用3小节分析一些问题,然后再给出具体的实现。
在前面《C++中的单例模式及按需释放模型(四)》已经给出了通用单例类实例获取器,我们来想想实际使用的时候的具体情况,对于一个单例类SingletonExample,使用这个模型在系统中只可能存在一个实例,但是其单例类实例获取器SingletonInstanceGetter有多少个实例呢,读者如果仔细阅读了前文,就会知道存在多个,也就是单例类实例同其实例获取器实例的关系是一对多,当所有的这个单例类实例获取器实例都释放了的时候,这个单例类实例也会被释放。
2、Windows中临界区的使用方法
要使用Windows中的临界区即CRITICAL_SECTION,一般都需要使用InitializeCriticalSection、EnterCriticalSection、LeaveCriticalSection、DeleteCriticalSection这4个API,具体函数含义这里不讲,如果不知道去查MSDN,这里只说一下4个API的调用位置,假设有主线程M,然后创建2个子线程T1、T2,3个线程工作时需要用到一个临界区C进行同步,那么调用函数的位置将是M中调用InitializeCriticalSection初始化C,然后创建T1、T2,在M、T1、T2中使用EnterCriticalSection/LeaveCriticalSection对C进行操作来同步,然后等T1、T2退出后M中调用DeleteCriticalSection来清理C,这个过程说明了什么,说明
A、InitializeCriticalSection/DeleteCriticalSection只在主线程中调用,工作中不会被调用,子线程不会调用这2个函数;
B、EnterCriticalSection/LeaveCriticalSection在工作中被调用,子线程只会调用到这2个函数。
3、如何在本单例模型中使用临界区
基于以上两点,我们分析在本单例模型中如何使用临界区
A、需要独立的临界区对象
要使用临界区,单例类实例获取器SingletonInstanceGetter的GetInstance方法中使用EnterCriticalSection/LeaveCriticalSection这个没有什么疑问,我们在单例类实例获取器SingletonInstanceGetter的构造和析构方法中使用InitializeCriticalSection/DeleteCriticalSection还是EnterCriticalSection/LeaveCriticalSection?这里应该还是使用EnterCriticalSection/LeaveCriticalSection,因为单例类实例获取器SingletonInstanceGetter的实例是在线程中创建和释放的,即它的构造析构函数是在线程中调用的,如果构造和析构方法中使用InitializeCriticalSection/DeleteCriticalSection,就会多次初始化和清理临界区,这是不可以的,那InitializeCriticalSection/DeleteCriticalSection在什么地方调用呢?应该在单例类实例获取器SingletonInstanceGetter的外部,即临界区不属于单例类实例获取器,不是组合关系,是独立于单例类实例获取器的,只是被其使用,所以我们需要为使用的临界区单独创建类管理。
B、需要一个临界区对象
那我们需要几个临界区对象呢?我们只需要一个,也就是对于一个单例类SingletonExample实例应该就对应一个临界区对象,即临界区对象也是一个单例类,同时对应多个单例类实例获取器SingletonInstanceGetter实例,这样既能保证多个单例类实例获取器SingletonInstanceGetter实例工作时可以使用这个临界区对象进行同步,同时不与其他单例类SingletonExampleTwo共用临界区,互相干扰,造成不必要的等待。
C、临界区对象的初始化和释放
我们既然建立了临界区对象,就可以在临界区对象的构造函数中调用InitializeCriticalSection,析构函数中调用DeleteCriticalSection,正如【四.2】中讲到的,这个临界区对象的创建应该在主线程中完成,然后在多线程中使用这个对象,等线程都退出后,在主线程中释放掉这个临界区对象。
4、临界区锁定类
A、临界区对象的实现
上面已经说明了临界区和某个单例类实例是一一对应关系,所以我们也使用模板类的方式建立临界区对象,模板参数就是单例类
临界区模板类的定义与实现
template <typename T>
class SingletonThreadLock
{
friend SingletonInstanceGetter<SingletonThreadLock<T>>::SingletonInstanceGetter