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]\n", 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]\n", 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]\n", 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
<[email protected]>
,这一行代码。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 <TestThread(void*)+72>

0x401975    mov    edi,0x6031f0

0x40197a   call   0x400a50
<[email protected]>

0x40197f    test   eax,eax

0x401981    setne  al

0x401984    test   al,al

0x401986    je     0x4019a2 <TestThread(void*)+72>

0x401988    mov    r12d,0x0

0x40198e    mov    edi,0x6031f8

0x401993   call   0x401b06 <TestStatic::TestStatic()>

0x401998    mov    edi,0x6031f0

0x40199d   call   0x400ae0 <[email protected]>

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 <[email protected]>

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 <TestThread(void*)+132>

0x4019d4        mov    edi,0x6031f0

0x4019d9        call   0x400b40 <[email protected]>

0x4019de        mov    rax,rbx

0x4019e1        mov    rdi,rax

0x4019e4        call   0x400b70 <[email protected]>

大家都喜欢使用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]\n", lpParam, test.m_num);

    return 0; 

}

这次的运行结果就正确了:

Thread[3] Num[999]

Thread[2] Num[999]

Thread[1] Num[999]

VC和gcc在保证函数static变量线程安全性上的区别,布布扣,bubuko.com

时间: 2024-10-26 19:41:24

VC和gcc在保证函数static变量线程安全性上的区别的相关文章

VC和gcc在保证功能static对线程安全的差异变量

VC和gcc不同,不能保证静态变量的线程安全性.这就给我们的程序带来了非常大的安全隐患和诸多不便.这一点应该引起我们的重视!尤其是在构造函数耗时比較长的时候.非常可能给程序带来意想不到的结果.本文从測试代码開始,逐步分析原理,最后给出解决方式. 多线程状态下.VC不能保证在使用函数的静态变量的时候,它的构造函数已经被运行完成,以下是一段測试代码: class TestStatic { public: TestStatic() { Sleep(1000*10); m_num = 999; } pu

局部变量和static变量

局部变量:指在程序中,只在特定过程或函数中可以访问的变量,是相对于全局变量而言的.在C++.C#.Ruby这些面向对象语言中,一般只使用局部变量.在面向对象编程中现在普遍采用的是软件开发方法,因此无需考虑是局部变量还是全局变量,说到变量,往往都是局部变量,局部变量只在局部起作用,超出了局部范围就会被释放.例如: 结果为: static变量:static变量其中一个变量为,保持变量内容的持久.这是static变量和局部变量之间的区别,例如: 结果为: 局部变量和static变量之间的区别主要原因在

辨析函数指针变量和指针型函数

在上一篇随笔(顺序表基本操作算法的代码实现)中,LocateElem()函数的第三个形参的形式是: Status (*compare)(Elemtype e,Elemtype temp); 这是一个函数指针变量,借此机会记录一下函数指针变量和指针型函数的区别. 一.写法上的区别 函数指针变量 指针型函数 int (*function)(int i); int  *function(int i){} 上面是一个例子,可看到函数指针变量只是在:*function处比指针型函数多了一对小括号,下面是两

[C] zintrin.h : 智能引入intrinsic函数。支持VC、GCC,兼容Windows、Linux、Mac OS X

博客来源:http://blog.csdn.net/zyl910/article/details/8100744 现在很多编译器支持intrinsic函数,这给编写SSE等SIMD代码带来了方便.但是各个编译器略有差异,于是我编写了zintrin.h,智能引入intrinsic函数. 一.各种编译器的区别 1.1 Visual C++(Windows) 最早支持intrinsic函数的VC编译器是VC 6.0.它在装上Visual Studio 6.0 Service Pack 5.Visual

用static声明的函数和变量小结

static 声明的变量在C语言中有两方面的特征: 1).变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值.这一点是它与堆栈变量和堆变量的区别. 2).变量用static告知编译器,自己仅仅在变量的作用范围内可见.这一点是它与全局变量的区别.Tips: A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度: B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度: C.设计和使用访问动态

在成员函数内定义static变量

成员函数内的局部变量可以是static的.如果将成员函数内的某个局部变量定义为静态变量,该类的所有对象在调用这个成员函数时将共享这个变量. 例3-40   本例在成员函数m中定义了一个static变量s,由于s定义在程序块内,它拥有程序块范围,因此它只能在m内部访问.每调用m一次,s就会相应地增加一次.又因为m是C的成员函数,所以,C的所有对象都共享这个静态局部变量.这样,对m的每一次调用访问的都是同一个s.相反,对于非静态局部变量x来说,每个C对象都拥有一个x.所以,在main中第一次调用c1

c++ 类与函数中static变量初始化问题(转)

首先static变量只有一次初始化,不管在类中还是在函数中..有这样一个函数: 1 void Foo() 2 { 3 static int a=3; // initialize 4 std::cout << a; 5 a++; 6 } 里的static int a=3只执行了一次.在main中调用Foo()两次,结果为34.将上面的函数改为 1 void Foo() 2 { 3 static int a; 4 a=3; // not initialize 5 std::cout <<

static变量、static函数与普通变量、普通函数的区别

转自:http://blog.163.com/sunshine_linting/blog/static/44893323201191294825184/ 全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量.全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式.这两者在存储方式上并无不同.这两者的区别虽在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的. 而静态全局变量则限制了其作用域, 即只在定义该

C# 中的局部static变量

其实这问题没什么可讨论的,C#不支持局部静态变量. 但还是想了一下C#为什么不支持局部静态变量,以下均是个人想法. C++和C支持局部静态变量,也就是在一个函数的内部声明一个静态变量,这种变量的特定如下: 静态局部变量在函数内定义,但不象自动变量那样,当调用时就存在,退出函数时就消失.静态局部变量始终存在着,也就是说它的生存期为整个程序的生命周期 静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的函数内使用该变量.退出该函数后,尽管该变量还继续存在,但不能使