c/c++语言开发共享C/C++编程教训—-函数内静态类对象初始化非线程安全(C++11之前)

不少程序员在编写程序的时候,会使用函数内静态(static)变量,既能满足函数内这个变量可以持久的记录某些信息,又使其访问范围的控制局限于函数内。但函数内静态类对象初始化是非线程安全的。 问题背景

不少程序员在编写程序的时候,会使用函数内静态(static)变量,既能满足函数内这个变量可以持久的记录某些信息,又使其访问范围的控制局限于函数内。但函数内静态类对象初始化是非线程安全的

问题背景

在我们产品中对log4cxx做了一些简单的封装 (采用vs2005编译),其中会调用到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。 那么现在重点来了,你必须知道两点:
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线程安全

博主采用了vs2015 (支持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  

这个功能在vs2015中默认开启,如果想要禁用这个功能, 可以添加额外的编译选项/zc:threadsafeinit-。 

总结

在c++ 11之前,尽量避免使用函数内静态对象。 尽量在条件允许的情况下,将编译器升级到支持c++ 11的vs2015或者以上吧。

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/c-cdevelopment/608109.html

(0)
上一篇 2021年5月15日
下一篇 2021年5月15日

精彩推荐