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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM