C++的静态初始化和注意事项

 

工作N年, 看到这个文章才幡然醒悟, 静态变量的初始化原来自己并没有真正的理解!

前因:

之所以在这个topic上反思, 起源于我随手翻看程杰的<大话设计模式> 21.7一节时, 提到 ---

C#与公共语言运行库提供一个"静态初始化"方法, 这种方法不需要开发人员现实地编写线程安全代码, 即可解决多线程环境下它是不安全的问题

具体说来, 就是在静态成员前面加上static readonly specifier, 这样就可保证这个静态成员在初始化时是线程安全的!

如:

 

1 class singleton
2 { 
3     private static readoly singleon instance = new singleton(); 
4     private singleton() {} 
5     public static singleton GetInstance() { return instance; }
6 }

 

文章中的这个"静态初始化"字眼让我无比地刺眼, 乍一看, 切, 这个不是什么新鲜的玩意啊, C++中, 静态变量初始化的地方多了去! 为什么在这里单独强调C#的"静态初始化"并号称他是"线程安全"的? 难道我之前理解的C++的"静态初始化"技术是"非线程安全的"?????    这个C#的"静态初始化"有什么过人之处????   --- 显然, 这里面必然有我不了解的地方!

 

首先回顾下自己对 静态初始化的理解:

一. "静态初始化"适用对象 ---- 静态变量

      UMM... 要回答什么是一个静态变量是如此的困难! "静态"这个概念本来就是很"confusing"的!!!!

      这里, 我想表达的"static"指的是, 其声明周期是static的, 即:自其初始化就一直存在, 直至程序结束才生命over的那些变量!

      按照这个定义, 静态变量包括以下3种: 

       1. global variables;           即 static variables with program scope   

            2. static variables with file scope

            3. static variables with block scope ;  这里的block, 可以是类内部; 函数内部;  或者以{, }包围起来的一个code block!

       注: 上述的"scope"指的是作用域.

 

二. 什么时候发生"静态初始化"?

     这个问题要具体情况具体分析:

 1. 如果初始化值是常量并且静态变量本身是基本数据类型(POD), 如: 

1 static int val = 10;
2 char strArray[] = "hello! world";

        那么这个初始化过程是在编译期间完成的, 这也就是通常所说的"编译时初始化".

 

    2.  如果不是情况1, 就一定是"运行时初始化"了!  

  而对于"运行时初始化", 借用来自"单例设计模式中经常会谈到的几个术语", 又可分为"饿汉式初始化" --- 即加载时初始化, 和"懒汉式初始化" --- 即使用时初始化.

        2.1   加载时初始化

 

              所谓"加载时初始化", 指的是在程序被加载时立即进行的初始化. 这个初始化发生在main函数之前. 由于这个初始化是在程序加载时一次过对变量进行初始化, 而即使程序任何地方都没访问过该变量, 仍然会发生, 因此形象地称之为"饿汉式初始化".  global variables 和 static variables with file scope 的初始化一定是加载时初始化.

       

    他包括如下情况:

              2.1.1) 静态变量是一个基本数据类型, 但是初始值非常量;

               举例1:

              int *globalBuffer1 = new int[1024];

              举例2:              

 

       int x = 3;
       int y = 4;
       int z = x + y;

     2.1.2)  静态变量的类型是一个类, 而非一个基本数据类型. 也就是说, 这个静态变量是一个类对象;  

        这种情况下, 即使是使用常量初始化, 如前面例子中的globalWelCoeMsg的初始化, 由于涉及到类的constructor调用, 所以必须是加载时初始化,而不是编译时初始化!

             举例1:

std::string globalWelcomeMsg = "Hello form ZX";

举例2:

 

 

class MyClass
{
public:
	MyClass();
    MyClass(int a, int b);

};

static MyClass * globalMyClassInstance1 = new MyClass();

MyClass * globalMyClassInstance2 = new MyClass;
MyClass * globalMyClassInstance3 = new MyClass();
MyClass   globalMyClassInstance4(0, 1);
static    MyClass fileScopedMyClassIstance5;

 

 

      2.2   运行时初始化

              static variables with block scrope 一定是运行时初始化. 这个初始化发生在这个变量第一次被引用时, 也就是说, 从程序执行模型角度看, 程序所在进程空间中, 哪个线程先访问了这个变量, 就是哪个线程来初始化这个变量!!!

              因此, 相对于加载初始化来说, 这种初始化是把真正的初始化动作推迟到第一次被访问时, 因而形象地称为"懒汉式初始化".

             举例1:

          

int myfunc()
{
     static int val1 = 12;                     //运行时初始化
     static bool val2 = false;                 //运行时初始化
     static std::string msg = "hello world";   //运行时初始化
     ......; // core logic
}

 

 

         举例2:

 

class ServiceObj
{
public:
    int Start()
   {
       static int isRun = false;     //运行时初始化
       if(!isRun)
       {
            ....; //core service logic;
       }
    }
};

 

====================================================== 回归现实的分割线 ==================================================

OK, 整理过去,是为了正视现在.  在整理到这里时, 真如小说中所说, 电光火石, 醍醐灌顶!!!  OMG,  既然这么多种初始化, 有的还是运行时初始化, 那么一定有与"运行时程序状态相关的"线程安全问题!!

而并非我之前一直的误解--- "静态初始化"就一定没有线程安全问题!!!  OK, 言归正传, 现重新更新我的Memory respository如下:

1. 如果是编译时和加载时初始化, 是不会存在线程安全这个issue的;  

       因为这两种初始化一定发生在Main函数执行之前, 这个时候尚未进入程序运行空间; 而这些初始化一定是在单线程环境下操作的!  --  都是在执行C Runtime的startup代码中的void mainCRTStartup(void)函数时所在的OS系统加载程序时的主线程空间上发生的!

2. 如果是运行时初始化, 因为无法保证访问这个静态变量所在的局部函数/全局函数/类成员函数/类静态成员函数  一定只会从某个特定的线程中被访问, 因此, 就一定会存在"线程安全"的issue!

最常用的例子就是单例类了:

 

 1 class Singleton 
 2 { 
 3 public: 
 4     static Singleton& GetInstance() 
 5     { 
 6         static Singleton instance; 
 7         return instance; 
 8     } 
 9     ......... 
10 }; 
11 
12 
13 void func()
14 {
15     ....
16     //call SomeMethod() when needed
17     Singleton::GetInstance()->SomeMethod();
18     ....
19 }

 

上述例子中, Singleton::instance变量是运行时初始化的, 是非线程安全的!

 

因此,很有可能存在, 多个线程同时调用Singleton::GetInsnance().method时, 某些线程取得的instance对象是尚未被初始化完毕的---即: singleton的构造函数尚未执行完毕!!! --- 而这个问题的后果是: 如果singleton保存有状态, 那么, 对于那些"取得的instance对象是尚未被初始化完毕的" 线程来说, 可能是一个致命的灾难!

  

因此, 对于上例, 要保证其"线程安全", 应该做如下改动:

 

 1 class LockClass                       
 2 {                                     
 3 public:                               
 4     LockClass()  {InitializeCriticalSection(m_cs); }
 5     lock()       {EnterCriticalSection(m_cs);      }
 6     unlock()     {LeaveCriticalSection(m_cs);      }
 7     ~LockClass() {DeleteCriticalSection(m_cs);     }
 8                                       
 9 private:                              
10     LPCRITICAL_SECTION m_cs;          
11 };                                    
12                                       
13                                       
14 LockClass globalLock;                 
15                                       
16 class Singleton                       
17 {                                     
18 private:                              
19     Singleton();                      
20                                       
21 public:                           
22     static Singleton& GetInstance()   
23     {                                 
24         globalLock.lock();            
25         static Singleton instance;    
26         globalLock.unlock();          
27         return instance;              
28     }                                 
29     int someMethod();                       
30 };        
31 
32 
33 void func()
34 {
35     ....
36     //call SomeMethod() when needed
37     Singleton::GetInstance()->SomeMethod();
38     ....
39 }

 

不过, 上述例子其实性能是很差的, --- 因此访问GetInstance()都要lock, unlock, 显然这不是programmer perfered的代码, 要改进这个, 需要用到double check locking技术, 以及将静态变量instance由类对象改为类对象指针!  改进后的代码为:

 1 class LockClass                       
 2 {                                     
 3 public:                               
 4     LockClass()  {InitializeCriticalSection(m_cs);  }
 5     lock()       {EnterCriticalSection(m_cs);        }
 6     unlock()     {LeaveCriticalSection(m_cs);       }   
 7     ~LockClass() {DeleteCriticalSection(m_cs);      }
 8                                       
 9 private:                              
10     LPCRITICAL_SECTION m_cs;          
11 };   
12 
13 class Singleton
14 {
15 private:
16     Singleton(){}
17 public:
18     static Singleton* m_instance;
19     static LockClass  m_lock;
20     static Singleton* getInstance();
21     
22     int SomeMethod();
23 };
24 
25 
26 //<! 在你的.cpp文件头部, 对static 变量进行初始化
27 Singleton* Singleton::m_instance = NULL;
28 LockClass  Singleton::m_lock;
29 
30 Singleton* Singleton::getInstance()
31 {
32     if(NULL == m_instance)
33     {
34         m_lock.lock();
35         if(NULL == m_instance)
36         {
37             m_instance = new Singleton;
38         }
39         m_lock.UnLock();
40     }
41     return m_instance;
42 }
43 
44 
45 void func()
46 {
47     ....
48     //call SomeMethod() when needed
49     Singleton::GetInstance()->SomeMethod();
50     ....
51 }

 


OK, 到此,看起来这个单例类已经考虑的很周全了, 既保证了"线程安全"又保证了"性能",  但, 你以为这就是"终结版"????? 
NO! 上面的例子无论使用自己写的锁, 还是用第三方锁, 如boost锁, 终归要借助外来和尚来确保线程安全, 这个违背了类内聚 & 最小化 的原则!
所以, 最终,我还是prefer这个实现方式  ---  借助 编译器的加载初始化时一定没有"线程安全"的issue 这个特点,  我更Perfer如下代码实现:

 

 1   class Singleton
 2 
 3 {
 4 private:   
 5     Singleton()
 6     { 
 7         //设置这个是为了避免Memmory leak, 当然这个泄露在APP退出时会由windows自动回收, 并且由于是singleton, 
 8         //这个leak并不是真正的run-time accumulated leak, 不会对程序性能有任何影响
 9         //但是, 坚持良好的编程习惯是很重要的, 这个, 你了解的 ...
10         atexit(ReleaseInstance);    
11     }
12     ~Singlton()
13     {
14     }
15     static void ReleaseInstance() { if(m_instance!=NULL) } 
16 public:
17     static Singleton* m_instance;
18     static Singleton* getInstance() { return m_instance; }
19     
20     int SomeMethod();
21 };
22 
23 //<! 在你的.cpp文件头部, 对static 变量进行初始化
24 //<! 不要惊奇, 虽然Singleton的constuctor定义为private, 但是下面这个语句, 看起来好像却可以在类外访问构造函数!!!
25 //<! 这是因为, 这个语句在编译器看来, 是对静态成员变量的初始化, 而不是一件简单的new对象.
26 //<! 这个初始化是发生在这个类的空间上, 自然可以访问类的私有构造函数!!!
27 //<! 另外, 在m_instance定义这里,加有const说明. const是保证这个单例对象指针不会被程序其他地方修改!
28 const Singleton* Singleton::m_instance = new Singleton();
29 
30 void func()
31 {
32     ....
33     //call SomeMethod() when needed
34     Singleton::GetInstance()->SomeMethod();
35     ....
36 }

 

好了, 回到开首, 现在就可以回答篇首的这个问题 ---- 为什么 那本书上讲 --- C#与公共语言运行库提供一个"静态初始化"方法, 在静态变量前面加上static readonly, 这种方法不需要开发人员现实地编写线程安全代码, 即可解决多线程环境下它是不安全的问题 !
这是因为, C#编译器碰到这样的"static readonly" speccifier时, 就会在该变量的初始化地方自动加上如上面C++例子中那样的加锁/解锁操作, 从而保证这个变量的初始化操作是线程安全的!!!!!


原来, 自己以前真的是"井底之蛙"!!!




推荐一篇很有用的文章:
英文原文: http://blogs.msdn.com/b/oldnewthing/archive/2004/03/08/85901.aspx 

中文翻译: http://www.cppblog.com/lymons/archive/2010/08/01/120638.html

另外一篇:    http://www.cnblogs.com/ccdev/archive/2012/12/19/2825355.html

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。