2013年5月6日星期一

关于C++ 静态成员对象的一段小故事

关于C++ 静态成员对象的一段小故事
  -- 20130322 静态对象做单例的危险


  • 微博吐槽
(引用)近几天遇到一工作问题令我困惑不已,且又不是我所熟悉过的,直观体验完全感觉不出问题存在,但从大量测试统计能微微发现问题确实是存在的。诸多排除法之后还是觉得无从下手,甚至有违常理的感觉...一遍一遍review代码是件痛苦事情,最后靠着一点点改动希望接近问题所在,再得以解决。[睡觉][汗][吐] 附图:

btw. 吐槽不是重点,重点的是作为经验告诫提醒自己或他人。还有,吐槽了让我明天继续满血(激情动力)查问题。

  • 故事细节
今天,Review代码时发现一个比较重要线索,线索不是最终问题(即下载速度变慢)但也是问题是程序存在的漏洞,但是很可能两者关系紧密。要知道在查“莫名”问题的整个过程是相当枯燥乏味的,还得顶着上面人的催赶压力,所以发现一丝可疑线索是足够令人兴奋起来的,何况该线索又是看似比较重要。发现了线索、解决问题并验证、提交测试验证跑速度对比,这个过程是轻快的,之后就等测试对比结果。由于晚上十点钟才提交开始部署测试,只能等到明早上班时间才能出结果。所以,先不管了安心回家睡觉,静待明日结果,如上图。

线索(问题)描述:DLL模块存在两个单例对象,都是数据量比较大的类型,这两个单例对象都是以全局的static静态对象存在的。所以,只要DLL被LoadLibrary加载起来了,即使什么事情也不做,这两个静态对象也被生成了(包括构造函数做的具体操作也统统被执行到了),直至DLL被FreeLibrary卸载掉它们才会被析构的。具体情况详见如下示例。
  • 示例说明
 class A {
private:
   A() {
     ... ...
   }
   static A _s_a;

 public:
   static A* get_instance() {
     return &_s_a;
   }
   void do_work() {
   }
 }
 int main() {
  A *a = A::get_instance();
  a->do_work();
  ... ...
 }
> 问题在哪里?
 当程序还未执行到main()的A *a = A::get_instance();这行代码,对象_s_a已经被构造出来了,构造函数A()的逻辑也被执行了。
 当程序逻辑出现比较多这样的类型的静态成员对象,而且之间存在依赖关系的话,程序很可能在启动时候出现莫名其妙的问题。

  • 安全的单例
> 更好的单例解决方案是什么?
*** 简单的单例模板:
template < class _Type >
class singleton
{
public:
    static _Type* get_instance()
    {
        // 通过观察汇编代码,static 变量初始化在多线程下并不安全,
        // 多个线程同时首次调用 get_instance,可能导致 instance 被初始化多次
        // 所以,强烈建议在进入程序时显式调用一次 get_instance。
        static _Type instance;
        return &instance;
    }
}
推荐在进入程序时,显式调用一下 get_instance
线程安全,dll 被 Load 多次时安全
singleton 的缺点是,对象只在程序退出时被析构,
所以无法控制它的析构时机,不安全!

*** 扩展的单例模板:
template < class _Type >
class singleton_ex {
public:
    static void create_instance();
    static _Type* get_instance();
    static void destroy_instance();
};
安全性最高,但是需注意其特殊用法,要求:
在进入程序时,必须调用 create_instance
在退出程序时,必须调用 destroy_instance
线程安全,即使dll 被 Load 多次时也是安全的。
只给出单例声明的模板,对于具体的实现细节在此不必给出,请自行YY脑补。另,强烈推荐在create_instance用从堆数据动态分配内存并构造(即new操作)单例对象。

(引用)singleton与singleton_ex的对比
一般简单的单子,可以考虑使用 singleton
但是正因为它简单,所以忽略了一个问题,那就是多个单子类的析构顺序问题
如果多个单子的首次 get_instance 调用顺序是不确定的,那么它们的析构
顺序同样也是不确的。如果多个类的析构互相依赖,那么就肯定会存在问题。

所以,当有多个互相依赖的单子时,有两个解决方案:
1.使用 singleton_ex,进入程序时,按照期望的顺序调用 create_instance,
  在退出程序时,按照相反的顺序调用 destroy_instance。
2.使用 singleton,进入程序时,按照期望的顺序调用 get_instance,这样可以
  保证在 dll 被卸载时,多个单子按照与构造相反的顺序被析构。

所谓的“进入程序”,在 Dll 中,就意味着 DllMain 函数中的 DLL_PROCESS_ATTACH 分支
所谓的“退出程序”,在 Dll 中,就意味着 DllMain 函数中的 DLL_PROCESS_DETACH 分支


所谓的“dll 被 Load 多次”:dll 中的单子会面临一个问题,例如某个进程中,dll 被多
个模块分别 LoadLibrary 多次。因为单子的实质是 static 变量,故在整个进程中只有一个,
这可能是设计者期望的,也可能是设计者不期望的,所以必须要注意。
singleton_ex 中的 create_instance 和 destroy_instance 就设计了 _ref() 来处理被
多个模块多次调用。当然,singleton 不存在该问题。
>>>>> 对于更加牛逼的单例实现方案,参见Loki库的Singleton。还有,可参考我之前的另一篇关于单例的文章“单例安全性”

(完)

没有评论:

发表评论