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

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

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

 

VCgcc不同,不能保证静态变量的线程安全性。这就给我们的程序带来了非常大的安全隐患和诸多不便。这一点应该引起我们的重视!尤其是在构造函数耗时比較长的时候。非常可能给程序带来意想不到的结果。本文从測试代码開始,逐步分析原理,最后给出解决方式。

 

多线程状态下。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函数的反汇编代码代码来分析问题。

不难看出,gccVC最大的差别就在于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模式。用的时候图方便,也喜欢直接在函数里面直接用个静态变量。

有的时候也必须使用静态变量。比方须要在程序退出的时候运行析构函数的情况。

可是多线程状态下。VCgcc不同。不能保证静态变量的线程安全性。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)
上一篇 2022年1月8日 下午2:00
下一篇 2022年1月8日 下午2:00


相关推荐

  • RabbitMQ 死信队列

    RabbitMQ 死信队列一 说明 RabbitMQ 是流行的开源消息队列系统 使用 erlang 语言开发 由于其社区活跃度高 维护更新较快 性能稳定 深得很多企业的欢心 当然 也包括我现在所在公司 手动滑稽 为了保证订单业务的消息数据不丢失 需要使用到 RabbitMQ 的死信队列机制 当消息消费发生异常时 将消息投入死信队列中 但由于对死信队列的概念及配置不熟悉 导致曾一度陷入百度的汪洋大海 无法自拔 很多文章都看起来可行 但是实际上却并不能帮我解决实际问题 最终 在官网文档中找到了我想要的答案 通过官网文档的学习 才发现对于死信

    2026年3月19日
    2
  • Text2SQL微调

    Text2SQL微调

    2026年3月15日
    3
  • Python调用豆包API常见问题有哪些?

    Python调用豆包API常见问题有哪些?

    2026年3月12日
    4
  • URG和PSH

    URG和PSHURG 与 PSHURG 和 PSH 是 TCP 协议中的两个控制位 URG 紧急位 当 URG 1 时 表明紧急指针字段有效 它告诉系统此报文中有紧急数据 应尽快传送 相当于高优先级的数据 而不需要按原来的排队顺序来传送 当 URG 1 时 发送应用进程告诉发送方的 TCP 有紧急数据要传送 于是紧急发送方就把紧急数据插入到本报文段数据的最前面 而紧急数据后面的数据依然是普通数据 这时要与首部中的紧急指针字

    2026年3月17日
    1
  • 新东方网课资源分享_新东方笔记大赛官网

    新东方网课资源分享_新东方笔记大赛官网
    新东方李老师的734条高频词组笔记(实在是太有用了!就转来了,没看过的,赶紧点吧~)

    1.abideby(=befaithfulto;obey)忠于;遵守。
    2.beabsentfrom….缺席,不在
    3.absenceofmind(=beingabsent-minded)心不在焉
    4.absorb(=takeuptheattentionof)吸引…的注意力(被动语态)beabsorbedin

    2025年11月5日
    4
  • idea-2021.12.13的激活码(JetBrains全家桶)

    (idea-2021.12.13的激活码)本文适用于JetBrains家族所有ide,包括IntelliJidea,phpstorm,webstorm,pycharm,datagrip等。IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/ide…

    2022年3月30日
    119

发表回复

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

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