浅析单例模式与线程安全(Linux环境c++版本)
2015-02-11 22:00
330 查看
什么是单例模式
单例模式是设计模式中一种常用模式,定义是Ensure a class has only one instance, and provide a global point of access to it.(确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例)用《设计模式之禅》里的话说,就是,在一个系统中,要求一个类有且仅有一个对象,如果出现多个就会出现“不良反应”,比如以下情景可能会用到单例模式
要求生成唯一序列号的环境
在整个项目中需要一个共享访问点或者共享数据,例如一个Web页面上的计数器,可以不用把每次的刷新纪录都记录到数据库中,使用单例模式保持计数器的值(要涉及到稍后谈到的线程安全)
创建一个对象需要都消耗资源过多,比如访问IO和数据库资源
需要定义大量的静态常量和静态方法(如工具类)
单例模式的优点
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的作用很明显减少了系统的性能开支,当一个对象的产生需要比较多的资源时,如读取数据,产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,永驻内存
避免对资源的多重占用,例如写一个文件动作,由于只有一个实例存在内存中,避免了对同一个资源的同时写操作
可以在系统设置全局的访问点,优化共享资源,例如可以设计一个单例类,负责所有数据表的映射处理
单例模式实现
单线程实现
说了这么多,但是模式如何设计?为了避免后面的派生类继承这个类,误将该类多次初始化(即内存中多个备份),我们应该将其构造函数设为私有,并且把唯一的一次初始化留到静态函数中,比如c++写出来是这样的
#include <iostream> template<typename T> class Singleton { public: static T& instance() { if (flag_init) { flag_init = false; flag_destory = true; init(); } return *value_; } static void init() { value_ = new T(); } static void destory() { if (flag_destory) { flag_destory = false; suicide(); } } static void suicide() { delete value_; } private: static T* value_; //只进行一次的初始化的指针 static bool flag_init; //表明是否可以进行初始化的标志位 static bool flag_destory; //表明是否进行过销毁的标志位 Singleton(){} //私有函数,导致无法多次初始化这个类 ~Singleton(){} //私有函数,导致无法多次销毁这个类 }; //静态变量必须在外面初始化 template<typename T> T* Singleton<T>::value_ = NULL; template<typename T> bool Singleton<T>::flag_destory = false; template<typename T> bool Singleton<T>::flag_init = true; //测试单例是否可用的测试类 class Example { public: Example(){value = 0;} ~Example(){} void tool() { value++; std::cout << value<< std::endl; } private: int value; }; int main(int argc, char *argv[]) { Example& ex1 = Singleton<Example>::instance(); ex1.tool(); Example& ex2 = Singleton<Example>::instance(); ex2.tool(); Singleton<Example>::destory(); Singleton<Example>::destory(); return 0; }
输出两次分别是1 和 2 表示操纵的是同一个对象,内存中只有一份,多次销毁,无效的销毁也会被忽略
多线程实现
但是这样就行了吗,如果在多线程会怎么样, 如果是多线程的话,在同时修改创建或者销毁的两个bool值时就可能发生错误。那么怎么解决呢,有人提出了DCLP(double checked locking pattern)机制,结合互斥锁mutex,询问两次的方法。
分析:
当多个线程同时进入instance()时,都会发现第一个if (value_ == NULL)为真,之后开始竞争,拿到锁的线程会直接通过第二个if (value_ == NULL)进行初始化,然后释放锁,其他的线程拿到之后会到达第二个if ,此时实例已经被初始化,直接返回实例,不会进行二次初始化
实现如下
<span style="font-size:12px;">#include <iostream> #include <unistd.h> #include "Mutex.h" template<typename T> class Singleton { public: static T& instance() { if (value_ == NULL) { MutexLockGuard guard(mutex_); //这个表示区域锁,实现后附代码,原理参见《Linux多线程服务端编程》陈硕著 if (value_ == NULL) { init(); } } return *value_; } static void init() { value_ = new T(); } private: static T* value_; static MutexLock mutex_; Singleton(){} ~Singleton(){} }; //静态变量必须在外面初始化 template<typename T> T* Singleton<T>::value_ = NULL; template<typename T> MutexLock Singleton<T>::mutex_; //测试单例是否可用的测试类 class Example : boost::noncopyable { public: Example(){value = 0;} ~Example(){} void tool() { value++; std::cout << value<< " "; } private: int value; }; void* thread(void*arg) { Example& ex3 = Singleton<Example>::instance(); ex3.tool(); return NULL; } int main(int argc, char *argv[]) { pthread_t tid; pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); pthread_create(&tid, NULL, thread, NULL); Example& ex1 = Singleton<Example>::instance(); ex1.tool(); sleep(1); return 0; }</span>编译时记得加参数。。。-pthread
Mutex.h代码
运行检测,好像多线程也没什么问题,但是这样真的可以吗,国外大神过来打脸了
meyers大神的一篇文章
meyers指出 new Singleton,这步(也就是init()函数里的new)在真正运行时会分解成三个行为
分配内存
构造实例对象
将指针指向分配的内存
这三个指令可能会被CPU重排,然后执行顺序发生变化比如 3->1->2
这在一般情况下不会有异常,因为乱序执行就是cpu的一种优化手段(详情自行查阅,内容很多,不展开叙述),而且在外层有互斥锁的保护。但是,我们的互斥锁的保护是有条件的,只有先经过第一个if 判断才能进入互斥锁保护范围,而这个条件却被3所影响,倘若cpu先执行了3,这时另一个cpu同时进行 1处的判断,发现指针已不为空,直接返回对象供上层使用,而这时你返回的却是一个根本还没构造完毕的对象!
pthread_once解决DCLP问题
pthread_once()由Pthreads库保证,某个函数在多线程下只执行一次,实现出来是这样的:#include <iostream> #include <boost/noncopyable.hpp> template<typename T> class Singleton { public: static T& instance() { pthread_once(&ponce_, &Singleton::init); return *value_; } static void init() { value_ = new T(); } private: static pthread_once_t ponce_; static T* value_; }; template<typename T> pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT; template<typename T> T* Singleton<T>::value_ = NULL;
有人会问了,pthread_once是怎么解决这个问题的呢,看源码把
<span style="font-size:12px;">int __pthread_once (once_control, init_routine) pthread_once_t *once_control; void (*init_routine) (void); { /* XXX Depending on whether the LOCK_IN_ONCE_T is defined use a global lock variable or one which is part of the pthread_once_t object. */ if (*once_control == PTHREAD_ONCE_INIT) { lll_lock (once_lock, LLL_PRIVATE); /* XXX This implementation is not complete. It doesn't take cancelation and fork into account. */ if (*once_control == PTHREAD_ONCE_INIT) { init_routine (); *once_control = !PTHREAD_ONCE_INIT; } lll_unlock (once_lock, LLL_PRIVATE); } return 0; }</span>看了以下,发现这个实现其实就是DCLP机制,不信你看
if (*once_control == PTHREAD_ONCE_INIT)出现了两次,原理上和之前我们自己写的差不多,那它为什么可靠呢,其实关键在于
<span style="font-size:12px;"> lll_lock (once_lock, LLL_PRIVATE);</span>
经过查询和查大神博客,这个是一个基于gcc内嵌指令的宏,不同硬件平台实现不一样,我们只记作用就好,作用是
不要把这段指令和前面的指令重排,也就是前面的指令必须按序执行
不要把变量缓存到寄存器
所以就避免了CPU指令乱序重排,有人对c++比较熟,会说那我给之前的变量加上volatile关键字就好了,对,但是linux下的c++,这里特指c++98标准的volatile很鸡肋,没有做到内存屏障的作用也就无法实现控制指令顺序的作用,所以自己很难实现,直接用pthread_once就好,如果是新的版本可以尝试最初的DCLP做法,这里有详细博文,我就不赘述了
c++11标准下DCLP
static 和 线程安全
出现了这么一种形式的Singleton,引起了讨论局部static对象会在第一次调用时初始化没错,但是构造函数执行过程中万一有另一个线程也调用了。singleton没构造完成就暴露出去了啊。
我们在Linux进行如下实验
就是2个线程同时调用instance,instance中的构造函数故意延时1s后初始化数值为100,结果g++保证了local static初始化的线程安全
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace::std; class Singleton { public: static bool flag; static Singleton& instance() { static Singleton singleton; return singleton; } int getTemp() { return temp; } private: int temp; Singleton(){ if (!Singleton::flag) { sleep(1); return; } temp = 100; } ~Singleton(){} }; bool Singleton::flag = false; void* thread(void*arg) { Singleton::flag = true; cout << "thread temp:" << Singleton::instance().getTemp() << endl; return NULL; } int main(int argc, char *argv[]) { pthread_t tid; pthread_create(&tid, NULL, thread, NULL); cout << "Main temp:" << Singleton::instance().getTemp() << endl; pthread_join(tid, NULL); return 0; }发现始终输出两个0,线程安全,我们用strace -F ./a.out 运行发现
futex是系统实现mutex的锁机制,我们发现自己程序中没有任何锁,但是运行时自动上锁了,经过查询,发现,这个和线程相关的Static性质是g++自己加上的patch,patch地址
除了g++,各编译器版本对静态函数的线程安全支持如下
下图网址在这里http://wiki.apache.org/stdcxx/C%2B%2B0xCompilerSupport
一些细枝末节
前面代码中出现了继承boost::noncopyable,这个是一个常用做法,noncopyable类把构造函数和析构函数设置成protected权限,这样子类可以调用,外面的类不能调用,说白了就是外面的调用者不能够通过赋值和copy构造新子类#include <boost/noncopyable> class Example: public boost::noncopyable { public: Example(){}; Example(int i){}; }; int main() { Example cl1(); Example cl2(1); //Example cl3(cl1); // error //Example cl4(cl2); // error return 0; }
最后奉上皓叔的单例模式讲解,这个是以java为例的
单例模式java
本文在阅读书籍和查阅网上资料完成,如有不足之处,请提出
参考资料:
《Linux多线程服务端编程》陈硕 著
《设计模式之禅》秦小波 著
相关文章推荐
- Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。
- 中缀表达式的计算,C++版本,Linux环境
- linux下一个C++线程安全的单例模式
- 转:Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。
- Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。
- Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。
- Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。
- Bridge桥接模式2 C++理解的第二个版本
- Linux上搭建C/C++IDE开发环境2
- Bridge桥接模式3 C++理解的第三个版本 代码更加专业了
- 切换linux中的当前java环境版本
- 观察者设计模式 C++版本
- linux 很不错下的C/C++开发开发环境 mono
- Linux环境下的C/C++基础调试技术1——初步了解(2011.01.10更新)
- linux环境下,c++库文件中的符号的含义
- Linux下C/C++开发环境的配置
- Bridge桥接模式1 C++理解的第一个版本
- Linux环境下使用eclipse开发C++动态链接库程序
- 分析模式-计量的C++实现——完美版本