C/C++プログラミングの教訓----関数内の静的クラスオブジェクトの非スレッドセキュリティの初期化(C++11以前)


多くのプログラマーは、プログラムを作成する際に、関数内の静的(static)変数を使用し、関数内のこの変数が一定の情報を永続的に記録し、アクセス範囲の制御を関数内に限定することができることを満たすことができます.しかし、関数内の静的クラスオブジェクトの初期化は非スレッドで安全です.
問題の背景
我々の製品ではlog 4 cxxに対していくつかの簡単なパッケージ(VS 2005コンパイルを採用)を行い、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が永遠に返されます.では、今ポイントが来ました.2つのことを知らなければなりません.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スレッドセキュリティ
ブロガーはVS 2015(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  

この機能はVS 2015でデフォルトでオンになっており、この機能を無効にするには、追加のコンパイルオプション/Zc:threadSafeInit-を追加することができます.詳細は/Zc:threadSafeInit(Thread-safe Local Static Initialization)を参照してください.
まとめ
  • C++11の前に、関数内の静的オブジェクトの使用をできるだけ避けます.
  • できるだけ条件が許す限り、コンパイラをC++11対応のVS 2015以上にアップグレードしましょう.