涉及到的知识:
静态变量的内存分配和初始化
全局变量、non-local static变量(文件域的静态变量和类的静态成员变量)在main执行之前的静态初始化过程中分配内存并初始化;local static 变量(局部静态变量)则是在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。
静态变量初始化的线程安全性说明
非局部静态变量一般在main执行之前的静态初始化过程中分配内存并初始化,可以认为是线程安全的;
局部静态变量在编译时,编译器的实现一般是在初始化语句之前设置一个局部静态变量的标识来判断是否已经初始化,运行的时候每次进行判断,如果需要初始化则执行初始化操作,否则不执行。这个过程本身不是线程安全的。C++0x之后该实现是线程安全的。
C++11标准针规定了局部静态变量初始化需要保证线程安全,C++03标准并无此说明,具体说明如
单例(Singleton)模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理
设计模式经典GoF定义的单例模式需要满足以下两个条件:
1)保证一个类只创建一个实例;
2)提供对该实例的全局访问点。
如果系统有类似的实体(有且只有一个,且需要全局访问),那么就可以将其实现为一个单例。实际工作中常见的应用举例:
1)日志类,一个应用往往只对应一个日志实例。
2)配置类,应用的配置集中管理,并提供全局访问。
3)管理器,比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例。
4)共享资源类,加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。
- class Singleton
- {
- public:
- static Singleton& Instance() //Instance()作为静态成员函数提供里全局访问点
- {
- if(ps == NULL) //如果还未实例化,即可实例话,反之提供实例的引用
- ps = new Singleton;
- return *ps; //返回指针的话可能会误被 delete,返回引用安全一点
- }
- private:
- Singleton(); //这里将构造,析构,拷贝构造,赋值函数设为私有,杜绝了生成新例
- ~Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- static Singleton* ps;
- };
- //源文件
- Singleton* Singleton::ps = NULL;
这种方法的好处在于直到 Instance() 被访问,才会生成实例,这种特性被称为延迟初始化(Lazy Initialization),这在一些初始化时消耗较大的情况有很大优势。
Lazy Singleton不是线程安全的,比如现在有线程A和线程B,都通过了 ps == NULL 的判断,那么线程A和B都会创建新实例。单例模式保证生成唯一实例的规则被打破了。
Eager Singleton(饿汉模式)
这种实现在编译器初始化的时候就完成了实例的创建,和上述的Lazy Singleton相反。
- //头文件
- class Singleton
- {
- public:
- static Singleton& Instance() //Instance()作为静态成员函数提供里全局访问点
- {
- return instance;
- }
- private:
- Singleton(); //这里将构造,析构,拷贝构造,赋值函数设为私有,杜绝了生成新例
- ~Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- static Singleton instance;
- };
- //源文件
- Singleton Singleton::instance;
Meyers Singleton
为了解决上面的问题,Scott Meyers在《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用local static对象(函数内的static对象)。当第一次访问 Instance() 方法时才创建实例。
- //头文件
- class Singleton
- {
- public:
- static Singleton& Instance() //Instance()作为静态成员函数提供里全局访问点
- {
- static Singleton instance;
- return instance;
- }
- private:
- Singleton(); //这里将构造,析构,拷贝构造,赋值函数设为私有,杜绝了生成新例
- ~Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- };
Double-Checked Locking Pattern(双检测锁模式)
回顾 Lazy Singleton 模式,考虑到线程安全,我们可以通过加锁来保护单例初始化这一过程,双检测锁模式就是在懒汉模式的基础上稍作修改得到:
- //头文件
- class Singleton
- {
- public:
- static Singleton& Instance() //Instance()作为静态成员函数提供里全局访问点
- {
- if(ps == NULL)
- {
- Lock(); //上锁
- if(ps == NULL) //如果还未实例化,即可实例话,反之提供实例的引用
- ps = new Singleton;
- Unlock(); //解锁
- }
- return *ps; //返回指针的话可能会误被 delete,返回引用安全一点
- }
- private:
- Singleton(); //这里将构造,析构,拷贝构造,赋值函数设为私有,杜绝了生成新例
- ~Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- static Singleton* ps;
- };
- //源文件
- Singleton* Singleton::ps = NULL;
以上的上锁和解锁仅用于说明,实际应用中可以使用互斥锁,单一信号量等方法去实现。
这里的两次 ps == NULL,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁模式”(Double-Checked Locking Pattern)。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。理论上问题解决了,但是在实践中有很多坑,如指令重排、多核处理器等问题让DCLP实现起来比较复杂比如需要使用内存屏障,详细的分析可以阅读这篇论文。
在C++11中有全新的内存模型和原子库,可以很方便的用来实现DCLP。这里不展开。有兴趣可以阅读这篇文章 《Double-Checked Locking is Fixed In C++11》 。
pthread_once
在多线程编程环境下,尽管 pthread_once() 调用会出现在多个线程中,init_routine()函数仅执行一次,pthread_once是很适合用来实现线程安全单例。(pthread_once 在一个进程里只会执行一次,其实现方式使用的就是互斥锁+条件变量的方法)
- //头文件
- pthread_once_t once = PTHREAD_ONCE_INIT;
- class Singleton
- {
- public:
- static Singleton& Instance() //Instance()作为静态成员函数提供一次实例化以及全局访问点
- {
- pthread_once(&once, &Init);
- return *ps;
- }
- static void Init()
- {
- ps = new Singleton;
- }
- private:
- Singleton();
- ~Singleton();
- Singleton(const Singleton&);
- Singleton& operator=(const Singleton&);
- static Singleton* ps;
- };
- //源文件
- Singleton* Singleton::ps = NULL;
总结:
单例模式的实现方法很多,要完成一个完美的实现很难,代码也会很复杂,但是掌握基础的实现还是很必要的。此外还要在实际应用中不断地去优化和探索。除了线程安全,一些场景下还有需要考虑资源释放,生命周期等相关问题,可以参见《Modern C++ Design》中对Singleton的讨论。