四、多线程安全的通用单例类实例获取器
例子工程的名称是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(); private: SingletonThreadLock(void) { InitializeCriticalSection(&m_csLock); } public: ~SingletonThreadLock(void) { DeleteCriticalSection(&m_csLock); } public: void Lock() { EnterCriticalSection(&m_csLock); } void Unlock() { LeaveCriticalSection(&m_csLock); } private: CRITICAL_SECTION m_csLock; };
不过多解释,什么位置调用什么临界区函数已经很明显,而且通过私有构造函数和友元函数的方式把临界区锁定模板类定义成单例类。
B、临界区对象的定义
这样在使用中要定义临界区单例实例就可以使用如下的方法
SingletonInstanceGetter<SingletonThreadLock<SingletonExampleOne>> sigOne; SingletonThreadLock<SingletonExampleOne> *pscsOne = sigOne.GetInstance();
pscsOne就是单例类SingletonExampleOne对应的临界区单例实例,启动子线程前就应该在主线程中使用以上方法定义一个临界区对象,因为是在主线程中调用,所以这里不需要使用线程安全的单例类实例获取器获得临界区对象,只需要使用SingletonInstanceGetter就可以了。
5、多线程安全的通用单例类实例获取器
有了临界区类了,我们就可以实现多线程安全的通用单例类实例获取器了,其模板参数仍然是单例类,但是其构造函数需要多一个参数,就是这个单例类对应的临界区对象,这样才可以使用这个临界区对象进行多线程同步
多线程安全的通用单例类实例获取器类的定义与实现
template <typename T> class SingletonThreadSafeInstanceGetter { public: SingletonThreadSafeInstanceGetter(SingletonThreadLock<T> *pscsSingleton) { if (NULL == pscsSingleton) { m_pcsSingleton = NULL; return; } m_pcsSingleton = pscsSingleton; m_pcsSingleton->Lock(); if (m_pInstance == NULL) { try { m_pInstance = new T(); } catch (...) //防止new分配内存可能出错的问题,如果是内存分配错误异常为std::bad_alloc { m_pInstance = NULL; } } //这里不管SingletonExample创建成功与否都要加1 m_uiReference++; m_pcsSingleton->Unlock(); } ~SingletonThreadSafeInstanceGetter(void) { if (NULL == m_pcsSingleton) return; m_pcsSingleton->Lock(); m_uiReference--; if (m_uiReference == 0) { if (m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; //非常重要,不然下次再次建立单例的对象的时候错误 } } m_pcsSingleton->Unlock(); m_pcsSingleton = NULL; } public: T *GetInstance() { if (NULL == m_pcsSingleton) return NULL; T *pInstance; m_pcsSingleton->Lock(); pInstance = m_pInstance; m_pcsSingleton->Unlock(); return pInstance; } private: static T *m_pInstance; static unsigned int m_uiReference; SingletonThreadLock<T> *m_pcsSingleton; }; template <typename T> T *SingletonThreadSafeInstanceGetter<T>::m_pInstance = NULL; template <typename T> unsigned int SingletonThreadSafeInstanceGetter<T>::m_uiReference = 0;
SingletonThreadSafeInstanceGetter类同SingletonInstanceGetter类的不同主要有如下3点
A、增加一个实例成员变量m_pcsSingleton,即单例类对应的临界区类SingletonThreadLock对象的指针成员;
B、SingletonThreadSafeInstanceGetter构造函数增加一个参数,这个参数就是单例类对应的临界区类SingletonThreadLock对象的指针,构造函数中首先就会把这个指针赋值给m_pcsSingleton,然后进行其他工作;
C、SingletonThreadSafeInstanceGetter的构造、析构、GetInstance方法执行的功能都和SingletonInstanceGetter基本一样,只是使用了单例类对应的临界区类SingletonThreadLock对象进行了同步。
值得说明的一点是,这里的多线程同步只是对于通过单例类实例获取器获取单例实例进行了同步,这个行为是多线程安全的,至于使用单例类时需要多线程同步,那必须开发人员在单例类中自己实现。
6、本模型多线程版本的使用方法
具体的单例类与《C++中的单例模式及按需释放模型(四)》中的单例类SingletonExample基本相同,只是把那个友元声明变成如下
friend SingletonThreadSafeInstanceGetter<SingletonExampleOne>:: SingletonThreadSafeInstanceGetter(SingletonThreadLock<SingletonExampleOne> *);
用于SingletonThreadSafeInstanceGetter就可以使用单例类SingletonExampleOne的私有构造方法创建对象了,如果需要这个单例类既能用于普通版本又能用于多线程版本,可以使用如下2句友元声明即可
friend SingletonInstanceGetter<SingletonExampleOne>::SingletonInstanceGetter(); friend SingletonThreadSafeInstanceGetter<SingletonExampleOne>:: SingletonThreadSafeInstanceGetter(SingletonThreadLock<SingletonExampleOne> *);
至于单例类SingletonExampleOne的其他内容完全和前文的一样,就不再列出代码,需要的请自己下载例子查看。
现在模型的构件都有了,就可以使用类似如下代码来使用这个模型了
unsigned __stdcall SingltonThread(void* pArguments) { SingletonThreadLock<SingletonExampleOne> *pscsOne = reinterpret_cast<SingletonThreadLock<SingletonExampleOne> *>(pArguments); //获取单例类实例 SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance(); //使用单例类实例,使用过程中如果需要多线程同步,单例类自己实现 pseSingleton1->SetVariable(1); //do something _endthreadex(0); return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE phThread[2] = {NULL, NULL}; unsigned threadID; SingletonInstanceGetter<SingletonThreadLock<SingletonExampleOne>> sigOne; SingletonThreadLock<SingletonExampleOne> *pscsOne = sigOne.GetInstance(); for (int i = 0; i < 2; ++i) phThread[i] = (HANDLE)_beginthreadex(NULL, 0, &SingltonThread, pscsOne, 0, &threadID); WaitForMultipleObjects(2, phThread, TRUE, INFINITE); for (int i = 0; i < 2; ++i) CloseHandle(phThread[i]); return 0; }
在主线程中针对单例类SingletonExampleOne通过单例类实例获取器获取其对应的临界区类SingletonThreadLock实例pscsOne,然后创建子线程,这里创建了2个子线程,并把这个pscsOne作为参数传递(如果还需要传递其他内容,使用结构体等方式把其他内容和临界区类实例一起作为参数)给子线程,子线程使用这个参数创建SingletonExampleOne对应的单例类实例获取器SingletonThreadSafeInstanceGetter的实例,然后通过其方法GetInstance得到单例类SingletonExampleOne的实例,后面使用这个实例进行工作,子线程执行结束时,单例类SingletonExampleOne的实例就跟随其单例类获取器实例一起释放掉了,等到所有子线程执行结束,主线程释放sigOne时就会释放临界区对象pscsOne,整个过程并不难理解。
7、测试程序例子及输出结果的分析
下面我们来看看具体的一个测试例子
程序如下
using dpex::SingletonInstanceGetter; using dpex::SingletonThreadLock; using dpex::SingletonThreadSafeInstanceGetter; unsigned __stdcall LoopFreeSingltonThread(void* pArguments) { SingletonThreadLock<SingletonExampleOne> *pscsOne = reinterpret_cast<SingletonThreadLock<SingletonExampleOne> *>(pArguments); for (int i = 0; i < 100000; ++i) { { SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance(); //do something } } _endthreadex(0); return 0; } int _tmain(int argc, _TCHAR* argv[]) { DWORD dw1, dw2, dw3, dw4, dw5, dw6; HANDLE phThread[5] = {NULL, NULL, NULL, NULL, NULL}; unsigned threadID; SingletonInstanceGetter<SingletonThreadLock<SingletonExampleOne>> sigOne; SingletonThreadLock<SingletonExampleOne> *pscsOne = sigOne.GetInstance(); cout << "现在开始测试,可能用时比较长,请耐心等待..." << endl << endl; dw1 = GetTickCount(); for (int i = 0; i < 100000; ++i) { { SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance(); //do something } } dw2 = GetTickCount(); for (int i = 0; i < 5; ++i) phThread[i] = (HANDLE)_beginthreadex(NULL, 0, &LoopFreeSingltonThread, pscsOne, 0, &threadID); WaitForMultipleObjects(5, phThread, TRUE, INFINITE); dw3 = GetTickCount(); for (int i = 0; i < 5; ++i) CloseHandle(phThread[i]); cout << "循环100000次新建释放单例对象所用的时间为:" << dw2- dw1 << "毫秒" << endl; cout << "5个线程,每个线程循环100000次新建释放单例对象所用的时间为:" << dw3- dw2 << "毫秒" << endl; cout << endl << "下面的测试,外部保持一个单例对象,所以不存在单例对象的新建释放,仅仅是获取。" << endl; SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance(); dw4 = GetTickCount(); for (int i = 0; i < 100000; ++i) { { SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton11 = sigSingletonInstanceGetter1.GetInstance(); //do something } } dw5 = GetTickCount(); for (int i = 0; i < 5; ++i) phThread[i] = (HANDLE)_beginthreadex(NULL, 0, &LoopFreeSingltonThread, pscsOne, 0, &threadID); WaitForMultipleObjects(5, phThread, TRUE, INFINITE); dw6 = GetTickCount(); for (int i = 0; i < 5; ++i) CloseHandle(phThread[i]); cout << "循环100000次仅获取单例对象所用的时间为:" << dw5- dw4 << "毫秒" << endl; cout << "5个线程,每个线程循环100000次仅获取单例对象所用的时间为:" << dw6- dw5 << "毫秒" << endl; cout << endl << "执行到这里,说明在多线程获取单例对象过程中,没有发生过多线程冲突。" << endl; char c; cin >> c; return 0; }
在本人的机器上其中一次的执行结果如下
现在开始测试,可能用时比较长,请耐心等待... 循环100000次新建释放单例对象所用的时间为:624毫秒 5个线程,每个线程循环100000次新建释放单例对象所用的时间为:593毫秒 下面的测试,外部保持一个单例对象,所以不存在单例对象的新建释放,仅仅是获取。 循环100000次仅获取单例对象所用的时间为:78毫秒 5个线程,每个线程循环100000次仅获取单例对象所用的时间为:499毫秒 执行到这里,说明在多线程获取单例对象过程中,没有发生过多线程冲突。
测试演示程序分为2大部分,每部分都是先在主线程中获得十万次单例类的实例,然后建立5个线程,每个线程也同样循环获取十万次单例类的实例,主线程等待所有子线程结束,然后输出某些信息,在主线程和子线程中的for循环都加入了一对大括号用来控制单例类实例获取器的生命周期,保证每次都把获取器的实例释放掉,使用这个方式来检测创建和释放对象所使用的时间,2部分的唯一区别就是第二部分在开始测试前,使用如下代码先获取了一个单例类的实例
SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance();
这样不管是主线程中的循环还是子线程中只是创建释放实例获取器类的实例,没有真正创建释放单例类的实例。
并在每一步都增加时间记录,然后输出每一步所花费的时间。
介绍了程序,下面我们介绍输出的结果
如果读者下载了测试例子,你的运行结果可能和我的不同,但是差不了太多,先来看看程序第一部分的结果,主线程循环100000次新建释放单例对象所用的时间为:624毫秒,而5个线程,每个线程循环100000次新建释放单例对象所用的时间为:593毫秒,这里第二个值小于前一个的值,运行多次也可能大于,但是也大不了多少,肯定不会超过2倍,但是5个线程总共创建释放了单例对象五十万次,主线程只是十万次,加上线程切换等额外开销,所用时间应该大于主线程时间的五倍才对,那这里是为什么反而少了这么多呢?其实道理也很简单,主线程的十万次是真实的十万次,确实创建释放单例对象十万次,而当5个子线程工作时,由于其中一个线程创建好单例对象还没有释放的时候切换到了其他线程,其他线程再创建的时候其实根本就没有创建,只是简单的引用一下而已,当然速度快了很多,毕竟创建释放对象才会耗费比较多的时间,而且这种情况发生的概率很高,所以时间耗费少了非常多,这样的情况下就出现了线程五十万次效率同十万次接近的情况了。
对于第二大部分的例子,当保证一个单例类对象创建好不释放,那么这个主线程的for循环中就不会出现单例类实例的创建释放,而是仅仅使用引用,所以其时间大幅减少,变成了仅仅78毫秒,但是5个线程还有499毫秒,同样是引用,但是毕竟运行了五十万次,并且还有线程切换,所以其时间是主线程十万次时间的5倍还要多,并且由于没有发生过真正的一次创建释放动作,所以其时间应该小于第一部分中5个线程用的时间。
这里还要注意一下例子中的中间部分的2行代码
SingletonThreadSafeInstanceGetter<SingletonExampleOne> sigSingletonInstanceGetter1(pscsOne); SingletonExampleOne *pseSingleton1 = sigSingletonInstanceGetter1.GetInstance();
和开头部分的2行代码
SingletonInstanceGetter<SingletonThreadLock<SingletonExampleOne>> sigOne; SingletonThreadLock<SingletonExampleOne> *pscsOne = sigOne.GetInstance();
虽然都是主程序中的代码,但是临界区对象pscsOne的生存期跟随其获取器是先于单例类SingletonExampleOne(生存期跟随其获取器实例sigSingletonInstanceGetter1)实例pseSingleton1创建的,释放时由于代码先后顺序,pscsOne也是晚于pseSingleton1释放的,这样是合理正确的,也就是单例类的生命周期必须在其对应的临界区实例的生命周期内,晚于临界区实例创建,早于临界区实例释放,不然就会出现问题,这里一定要控制好。