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

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

大家好,又见面了,我是全栈君,今天给大家准备了Idea注册码。

 

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 <__cxa_guard_acquire@plt>,这一行代码。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 <__cxa_guard_acquire@plt>

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 <__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 <printf@plt>

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 <__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]\n", lpParam, test.m_num);

    return 0; 

}

 

 

 

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

Thread[3] Num[999]

Thread[2] Num[999]

Thread[1] Num[999]

版权声明:本文博客原创文章,博客,未经同意,不得转载。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/117263.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • vc++可以编辑c语言吗?_vc6.0使用教程详解

    vc++可以编辑c语言吗?_vc6.0使用教程详解如何编写自己的VCL控件    用过Delphi的朋友们,大概对Delphi的最喜欢Delphi的不是他的强类型的pascal语法,而是强大的VCL控件,本人就是一位VCL控件的爱好者。    VCL控件的开源,给我们带来了享之不尽的优点。不像曾经的ole控件以及ActiveX,你全然能够重写Delphhi标准控件,并且网上这方面的资源非常多。    关于怎样编写VCL控…

    2022年9月25日
  • MySQL数据库面试题(2020最新版)

    MySQL数据库面试题(2020最新版)文章目录数据库基础知识为什么要使用数据库什么是SQL?什么是MySQL?数据库三大范式是什么mysql有关权限的表都有哪几个MySQL的binlog有有几种录入格式?分别有什么区别?数据类型mysql有哪些数据类型引擎MySQL存储引擎MyISAM与InnoDB区别MyISAM索引与InnoDB索引的区别?InnoDB引擎的4大特性存储引擎选择索引什么是索引?索引有哪些优缺点?索引使用场景(重点)…

    2022年6月13日
  • 云开发定时触发器_python抢手机脚本

    云开发定时触发器_python抢手机脚本你也在做微信云开发?你也要使用事件触发器实现一些业务逻辑?你也发现微信开发文档屁都没有一个?那你来看看吧

    2022年10月30日
  • URL 字符编码建议收藏

    URL编码会将字符转换为可通过因特网传输的格式。URL-统一资源定位器Web浏览器通过URL从web服务器请求页面。URL是网页的地址,比如http://www.cnblogs.co

    2021年12月20日
  • win10游戏运行库合集(游戏运行库合集有什么用)

    大家好,今天给小伙伴们带来几套最新的微软常用运行库,解决多数程序莫名崩溃、游戏闪退问题。如果你遇到了莫名其妙的系统崩溃、无法判断或无法复现的win系统闪退崩溃等问题,或者你看见过以下画面:总之就是缺文件,打不开,当然,按照它的建议重新安装程序也是没什么卵用的。那么,把今天这个包里的东西,都安装一边,肯定100%解决问题!咱先说说原因。微软提供了大量的封装函数功能,让开发者们不需要再编写这些函数,在程序运行时直接调用就好了。但许多绿色或者简化的系统、软件、游戏为了..

    2022年4月13日
  • 防火墙的AAA认证[通俗易懂]

    防火墙的AAA认证[通俗易懂]防火墙的AAA认证防火墙的AAA提供基于三种数据库认证:LOCAL,TACACS+,RADIUS.使用tacacs+,radius两种认证协议来进行专门的AAA服务器与NAS之间进行接入访问认证。基本认证原理:客户机发起验证请求到NAS,而NAS帮助收集用户的验证信息并使用pre-sharekey与AAA服务器进行交互,AAA会对NAS提交上来的…

    2022年6月5日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号