VC와 gcc가 함수static 변수 라인의 안전성을 확보하는 데 있어서의 차이


VC와 gcc는 다르기 때문에 정적 변수의 라인 안전성을 보장할 수 없습니다.이것은 우리의 절차에 매우 큰 안전 위험과 많은 불편을 가져왔다.이 점은 마땅히 우리의 중시를 불러일으켜야 한다!특히 구조 함수가 비교적 오래 걸릴 때 프로그램에 예상치 못한 결과를 가져올 가능성이 높다.본고는 테스트 코드부터 원리를 점차적으로 분석하고 마지막으로 해결 방안을 제시한다.
 
다중 스레드 상태에서 VC는 함수의 정적 변수를 사용할 때 구조 함수가 실행되었음을 보장할 수 없습니다. 다음은 테스트 코드입니다.
 
 class TestStatic
{
public:
    TestStatic()
    {
       Sleep(1000*10);
       m_num = 999;
    }

public:
    int m_num;
};

 

DWORD WINAPI TestThread( LPVOID lpParam ) 
{ 
    static TestStatic test;
    printf("Thread[%d] Num[%d]
", lpParam, test.m_num); return 0; } int _tmain(int argc, _TCHAR* argv[]) { DWORD dwThreadId; for (int i=1; i<=3; i++) { CreateThread(NULL,0,TestThread,(LPVOID)i,0,&dwThreadId); } for (int i =0; i<10; i++) { Sleep(1000*10000); } return 0; }

 
 
테스트 코드는 고의로 구조 함수에서 비교적 긴 시간 지연을 만들었고 프로그램이 실행된 결과는 다음과 같다.
Thread[2] Num[0]
Thread[3] Num[0]
Thread[1] Num[999]
 
그 결과 스레드 2와 스레드 3은 정적 변수의 구조 함수가 실행되지 않았을 때 이미 이 변수의 실례를 사용해서 잘못된 결과를 얻었다.
아래 나열된 TestThread 함수의 어셈블러 코드에서 문제점을 쉽게 파악할 수 있습니다.정적 변수 실례가 존재하지 않을 때 프로그램은 실례를 만들고 구조 함수를 호출합니다.실례가 존재할 때 실례 생성과 구조 함수 호출 두 단계를 직접 건너뛴다.
위의 출력 결과와 결합하여 스레드 1은 함수TestThread를 가장 먼저 호출하기 때문에 실례test를 생성하고TestStatic 클래스 구조 함수를 호출하기 시작했고 구조 함수는sleep에 끼었다.그 다음에 스레드 2와 스레드 3은 TestThread 함수를 호출했지만 이때 구조 함수가 실행되지 않았지만 정적 변수의 실례가 존재했기 때문에 생성 실례와 조정 함수를 건너뛰고 printf 함수의 호출에 가서 초기화되지 않은 변수 값을 출력했다(이곳은 0).sleep가 완성된 후 구조 함수가 실행되고 변수 값은 999로 설정되며 라인 1만 정확한 결과를 얻었습니다 999.
 
 
     static TestStatic test;
00D48A7D  mov         eax,dword ptr [$S1 (0D9EA94h)]
00D48A82  and         eax,1
00D48A85  jne        TestThread+6Ch (0D48AACh)
00D48A87  mov         eax,dword ptr [$S1 (0D9EA94h)]
00D48A8C  or          eax,1
00D48A8F  mov         dword ptr [$S1 (0D9EA94h)],eax
00D48A94  mov         dword ptr [ebp-4],0
00D48A9B  mov         ecx,offset test (0D9EA98h)
00D48AA0  call        TestStatic::TestStatic (0D2DF6Dh)
00D48AA5  mov         dword ptr [ebp-4],0FFFFFFFFh
     printf("Thread[%d] Num[%d]", lpParam, test.m_num);
00D48AAC mov         esi,esp
00D48AAE  mov         eax,dword ptr [test (0D9EA98h)]
00D48AB3  push        eax 
00D48AB4  mov         ecx,dword ptr [ebp+8]
00D48AB7  push        ecx 
00D48AB8  push        offset string "thread[%d] num[%d]"(0D8A0A0h)
00D48ABD  call        dword ptr [MSVCR90D_NULL_THUNK_DATA (0DA0B3Ch)]
……
 
비슷한 코드입니다. 우리는 linux에서 gcc 컴파일러를 사용하여 효과가 어떠한지 봅시다.
 
class TestStatic
{
public:
         TestStatic()
         {
                   sleep(10);
                   m_num = 999;
         }
public:
         int m_num;
};

static void* TestThread( void* lpParam ) 
{ 
         static TestStatic test;
         printf("Thread[%d] Num[%d]
", lpParam, test.m_num); return 0; } int main (int argc, char *argv[]) { pthread_attr_t ThreadAttr; pthread_attr_init(&ThreadAttr); pthread_attr_setdetachstate(&ThreadAttr, PTHREAD_CREATE_DETACHED); pthread_t tid; for (int i=1; i<=3; i++) { pthread_create(&tid, &ThreadAttr, TestThread, (void*)i); } sleep(60*60*24); return(0); }

 
최종 결과에 따르면 gcc가 컴파일한 프로그램과 VC는 서로 다른 결과를 보였고 모든 라인은 정확한 수치를 얻었다. 이를 통해 알 수 있듯이 gcc는 함수 내부 정적 변수의 라인 안전성을 진정으로 보장했다. 프로그램 운행 결과는 다음과 같다.
 
Thread[3] Num[999]
Thread[2] Num[999]
Thread[1] Num[999]
 
마찬가지로 TestThread 함수의 어셈블리 코드 코드에서 문제를 분석합니다.gcc와 VC의 가장 큰 차이점은call 0x400a50, 이 줄 코드.gcc는 정적 변수 실례를 만들기 전에 자물쇠를 가져오고 구조 함수를 실행해야 실례 생성에 성공했다고 생각합니다.분명히 이 자물쇠는 gcc가 자동으로 추가한 코드입니다.따라서 구조 함수가 실행되지 않아 모든 스레드가test 변수를 얻지 못하고 VC 프로그램처럼 잘못된 결과를 출력하지 않습니다.
 
0x40195a    push   rbp
0x40195b    mov    rbp,rsp
0x40195e    push   r12
0x401960    push   rbx
0x401961    sub    rsp,0x10
0x401965    mov    QWORD PTR [rbp-0x18],rdi
0x401969    mov    eax,0x6031f0
0x40196e    movzx  eax,BYTE PTR [rax]
0x401971    test   al,al
0x401973    jne    0x4019a2
0x401975    mov    edi,0x6031f0
0x40197a   call   0x400a50 <__cxa_guard_acquire@plt>
0x40197f    test   eax,eax
0x401981    setne  al
0x401984    test   al,al
0x401986    je     0x4019a2
0x401988    mov    r12d,0x0
0x40198e    mov    edi,0x6031f8
0x401993   call   0x401b06
0x401998    mov    edi,0x6031f0
0x40199d   call   0x400ae0 <__cxa_guard_release@plt>
0x4019a2    mov    edx,DWORD PTR [rip+0x201850]        # 0x6031f8 <_ZZL10TestThreadPvE4test>
0x4019a8    mov    rax,QWORD PTR [rbp-0x18]
0x4019ac    mov    rsi,rax
0x4019af    mov    edi,0x401d9c
0x4019b4    mov    eax,0x0
0x4019b9    call   0x400a40
0x4019be        mov    eax,0x0
0x4019c3         add    rsp,0x10
0x4019c7         pop    rbx
0x4019c8         pop    r12
0x4019ca         pop    rbp
0x4019cb         ret   
0x4019cc         mov    rbx,rax
0x4019cf          test   r12b,r12b
0x4019d2        jne    0x4019de
0x4019d4        mov    edi,0x6031f0
0x4019d9        call   0x400b40 <__cxa_guard_abort@plt>
0x4019de        mov    rax,rbx
0x4019e1        mov    rdi,rax
0x4019e4        call   0x400b70 <_Unwind_Resume@plt>
 
 
모두들 Singleton 모드를 즐겨 사용하는데 사용할 때 그림이 편리하고 함수 안에서 직접 정적 변수를 사용하는 것도 좋아한다.프로그램이 종료될 때 분석 함수를 실행해야 하는 경우도 정적 변수를 사용해야 한다.
그러나 다중 스레드 상태에서 VC와 gcc는 다르기 때문에 정적 변수의 스레드 안전성을 보장할 수 없다.VC의 이 결함으로 인해singleton 모드를 사용할 때 gcc처럼 정적 함수 구성원 변수를 직접 사용할 수 없습니다.이것은 우리의 절차에 매우 큰 안전 위험과 많은 불편을 가져왔다.이 점은 마땅히 우리의 중시를 불러일으켜야 한다!특히 구조 함수가 비교적 오래 걸릴 때 프로그램에 예상치 못한 결과를 가져올 가능성이 높다.우리는 반드시 변통된 방법을 사용하여 클래스의 초기화 과정을 스스로 통제해야 한다.
이전에 내가 이 문제를 해결할 때 전역 변수의 자물쇠를 직접 정의했지만 전역 변수 코드를 정의하는 것은 아름답지 않다. 왜냐하면 좋은 스타일이 아니기 때문이다.이 동시에 잠금 해제도 효율에 상당한 영향을 미친다.
다음은 함수 내부의 기본 유형 변수를 사용하여 복잡한 인스턴스의 생성을 제어하는 데 사용할 수 있는 샘플 코드를 보여 줍니다.
 
class ClassStatic
{
public:
    ClassStatic()
    {
       Sleep(1000*10);
       m_num = 999;
    } 
public:
    int m_num;
};
  
DWORD WINAPI TestThread( LPVOID lpParam ) 
{ 
    static volatile long single = 1;

    while(single != 0)
    {
       if (1 == _InterlockedCompareExchange(&single, 2, 1))
       {
           break;
       } 
       else
       {
            for ( unsigned int i = 0; i < 1024; i++ )
           {
                _mm_pause();
           }

           while (single != 0)
           {
              Sleep(1);
           }
       }
    }
 
    static ClassStatic test;

    single && (single = 0);
    
    printf("Thread[%d] Num[%d]
", lpParam, test.m_num); return 0; }

 
 
 
이번 운영 결과는 정확합니다.
Thread[3] Num[999]
Thread[2] Num[999]
Thread[1] Num[999]

좋은 웹페이지 즐겨찾기