博客
关于我
C/C++编程教训----函数内静态类对象初始化非线程安全(C++11之前)
阅读量:637 次
发布时间:2019-03-14

本文共 4715 字,大约阅读时间需要 15 分钟。

不少程序员在编写程序的时候,会使用函数内静态(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-。 详细的可以参考。

总结

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

转载地址:http://xygoz.baihongyu.com/

你可能感兴趣的文章
MySQL _ MySQL常用操作
查看>>
MySQL – 导出数据成csv
查看>>
MySQL —— 在CentOS9下安装MySQL
查看>>
mysql 不区分大小写
查看>>
mysql 两列互转
查看>>
MySQL 中开启二进制日志(Binlog)
查看>>
MySQL 中文问题
查看>>
MySQL 中日志的面试题总结
查看>>
mysql 中的all,5分钟了解MySQL5.7中union all用法的黑科技
查看>>
Mysql 中的日期时间字符串查询
查看>>
MySQL 中锁的面试题总结
查看>>
MySQL 中随机抽样:order by rand limit 的替代方案
查看>>
MySQL 为什么需要两阶段提交?
查看>>
mysql 为某个字段的值加前缀、去掉前缀
查看>>
mysql 主从
查看>>
mysql 主从 lock_mysql 主从同步权限mysql 行锁的实现
查看>>
mysql 主从互备份_mysql互为主从实战设置详解及自动化备份(Centos7.2)
查看>>
mysql 主从关系切换
查看>>
mysql 主键重复则覆盖_数据库主键不能重复
查看>>
Mysql 优化 or
查看>>