1. 程式人生 > >C/C++程式設計教訓----函式內靜態類物件初始化非執行緒安全(C++11之前)

C/C++程式設計教訓----函式內靜態類物件初始化非執行緒安全(C++11之前)

不少程式設計師在編寫程式的時候,會使用函式內靜態(static)變數,既能滿足函式內這個變數可以持久的記錄某些資訊,又使其訪問範圍的控制侷限於函式內。但函式內靜態類物件初始化是非執行緒安全的

問題背景

在我們產品中對log4cxx做了一些簡單的封裝 (採用VS2005編譯),其中會呼叫到getWarn這個介面。由於這個函式存在非執行緒安全的問題,導致程式Crash。為了更好的描述問題,博主後面採用一個簡單的例子去做分析:為什麼這個是非執行緒安全的。

LevelPtr Level::getWarn() {
    static LevelPtr level(new Level(Level::WARN_INT
, LOG4CXX_STR("WARN"), 4)); return level; }

例子

這裡們寫了一段樣例程式碼,採用VS2005,為了避免程式被優化,博主採用的是Debug模式編譯。

class TestObject
{
public:
    int m_iVal;
    TestObject()
    {
        m_iVal = 4;
    }
};

TestObject TestFunction()
{
    static TestObject obj;
    return obj;
}

以上程式碼簡單來說,就是返回一個TestObject

的類物件。TestFunction中永遠返回一個靜態物件obj。 那麼現在重點來了,你必須知道兩點:
1. obj是在函式TestFunction第一次被呼叫的時候才會呼叫建構函式
2. obj在應用程式啟動的時候,obj物件記憶體中的值都為0。並且這裡的obj在初始化的時候(這裡可以認為呼叫建構函式)是非執行緒安全的。

分析非執行緒安全

要分析這個問題,我們得通過VS的反彙編來檢視,我在以下的程式碼中加了註釋來直接解釋這個問題。

TestObject TestFunction()
{
0000000140001800  mov         qword ptr [rsp+8],rcx 
0000000140001805  push        rdi  
0000000140001806  sub
rsp,30h 000000014000180A mov rdi,rsp 000000014000180D mov rcx,0Ch 0000000140001817 mov eax,0CCCCCCCCh 000000014000181C rep stos dword ptr [rdi] 000000014000181E mov rcx,qword ptr [rsp+40h] 0000000140001823 mov qword ptr [rsp+20h],0FFFFFFFFFFFFFFFEh static TestObject obj; //=========================== 這個地方從記憶體中讀取一個值,可以理解為編譯器給程式自動加了一個變數bInit(判斷obj物件是否初始化了,bInit初始值為0),將bInit讀取到eax,然後判斷為1表示已經初始化,則直接返回物件;如果為0,則按順序繼續執行。 //=========================== 000000014000182C mov eax,dword ptr [$S1 (14000F2A4h)] 0000000140001832 and eax,1 0000000140001835 test eax,eax 0000000140001837 jne TestFunction+55h (140001855h) //=========================== 將bInit值設定為1, 並且呼叫obj建構函式, 完成物件初始化 //=========================== 0000000140001839 mov eax,dword ptr [$S1 (14000F2A4h)] 000000014000183F or eax,1 0000000140001842 mov dword ptr [$S1 (14000F2A4h)],eax 0000000140001848 lea rcx,[obj (14000F2A0h)] 000000014000184F call TestObject::TestObject (1400011EFh) 0000000140001854 nop return obj; 0000000140001855 mov rax,qword ptr [rsp+40h] 000000014000185A mov ecx,dword ptr [obj (14000F2A0h)] 0000000140001860 mov dword ptr [rax],ecx 0000000140001862 mov rax,qword ptr [rsp+40h] }

看了以上彙編和解釋之後,大家應該能明白這裡存在一個Race Condition。當多個執行緒,同時呼叫TestFunction這個函式,當執行緒A執行完0000000140001842 mov dword ptr [$S1 (14000F2A4h)],eax, 執行緒B剛好進入TestFunction執行,以為obj已經初始化了,則直接返回物件,其實這個時候物件內部的m_iVal為0, 並非程式設計師的本意。

C++ 11執行緒安全

博主採用了VS2015 (支援C++ 11)編譯了以上的程式碼,得到如下彙編, 其通過_Init_thread_header_Init_thread_footer來保證區域性的靜態物件的初始化執行緒安全。具體實現google並沒有找到,有興趣的同學可以彙編跟進去再研究研究。

TestObject TestFunction()
{
00007FF65F411830  mov         qword ptr [rsp+8],rcx  
00007FF65F411835  push        rbp  
00007FF65F411836  push        rdi  
00007FF65F411837  sub         rsp,108h  
00007FF65F41183E  lea         rbp,[rsp+20h]  
00007FF65F411843  mov         rdi,rsp  
00007FF65F411846  mov         ecx,42h  
00007FF65F41184B  mov         eax,0CCCCCCCCh  
00007FF65F411850  rep stos    dword ptr [rdi]  
00007FF65F411852  mov         rcx,qword ptr [rsp+128h]  
00007FF65F41185A  mov         qword ptr [rbp+0C8h],0FFFFFFFFFFFFFFFEh  
    static TestObject obj;
00007FF65F411865  mov         eax,104h  
00007FF65F41186A  mov         eax,eax  
00007FF65F41186C  mov         ecx,dword ptr [_tls_index (07FF65F41C1E0h)]  
00007FF65F411872  mov         rdx,qword ptr gs:[58h]  
00007FF65F41187B  mov         rcx,qword ptr [rdx+rcx*8]  
00007FF65F41187F  mov         eax,dword ptr [rax+rcx]  
00007FF65F411882  cmp         dword ptr [obj+4h (07FF65F41C180h)],eax  
00007FF65F411888  jle         TestFunction+88h (07FF65F4118B8h)  
00007FF65F41188A  lea         rcx,[obj+4h (07FF65F41C180h)]  
00007FF65F411891  call        _Init_thread_header (07FF65F41101Eh)  
00007FF65F411896  cmp         dword ptr [obj+4h (07FF65F41C180h)],0FFFFFFFFh  
00007FF65F41189D  jne         TestFunction+88h (07FF65F4118B8h)  
00007FF65F41189F  lea         rcx,[obj (07FF65F41C17Ch)]  
00007FF65F4118A6  call        TestObject::TestObject (07FF65F411028h)  
00007FF65F4118AB  nop  
00007FF65F4118AC  lea         rcx,[obj+4h (07FF65F41C180h)]  
00007FF65F4118B3  call        _Init_thread_footer (07FF65F411078h)  
    return obj;
00007FF65F4118B8  mov         rax,qword ptr [rbp+100h]  
00007FF65F4118BF  mov         ecx,dword ptr [obj (07FF65F41C17Ch)]  
00007FF65F4118C5  mov         dword ptr [rax],ecx  
00007FF65F4118C7  mov         rax,qword ptr [rbp+100h]  
}
00007FF65F4118CE  lea         rsp,[rbp+0E8h]  
00007FF65F4118D5  pop         rdi  
00007FF65F4118D6  pop         rbp  
00007FF65F4118D7  ret  

這個功能在VS2015中預設開啟,如果想要禁用這個功能, 可以新增額外的編譯選項/Zc:threadSafeInit-。 詳細的可以參考/Zc:threadSafeInit (Thread-safe Local Static Initialization)

總結

  1. 在C++ 11之前,儘量避免使用函式內靜態物件。
  2. 儘量在條件允許的情況下,將編譯器升級到支援C++ 11的VS2015或者以上吧。