C/C++编程教训----函数内静态类对象初始化非线程安全(C++11之前)
2017-09-03 16:57
483 查看
不少程序员在编写程序的时候,会使用函数内静态(
以上代码简单来说,就是返回一个
1. obj是在函数
2.
看了以上汇编和解释之后,大家应该能明白这里存在一个Race Condition。当多个线程,同时调用
这个功能在VS2015中默认开启,如果想要禁用这个功能, 可以添加额外的编译选项
尽量在条件允许的情况下,将编译器升级到支持C++ 11的VS2015或者以上吧。
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-。 详细的可以参考/Zc:threadSafeInit (Thread-safe Local Static Initialization)。
总结
在C++ 11之前,尽量避免使用函数内静态对象。尽量在条件允许的情况下,将编译器升级到支持C++ 11的VS2015或者以上吧。
相关文章推荐
- 条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用 (转自effective c++ second edition)
- c++中子对象的初始化可在复合类的构造函数的函数体内进行吗?还是子对象的初始化只能在初始化列表中进行?
- C++小课堂--第一期---面向对象之前 --- 实现函数返回结构体,实现大数据的传输
- C++模板编程及函数对象
- C++ boost 组件简介:函数对象及高级编程
- C++ (1) 函数对象与谓词
- C++对象模型(四):class成员初始化列表(Member Initialization List)
- c++ 类对象数组的初始化
- [C++对象模型][5]堆栈与函数调用
- * 期末考试 编程题#7:字符串排序(Coursera 程序设计与算法 专项课程3 C++程序设计 郭炜、刘家瑛;函数对象作参数)
- 【C++对象模型】之虚函数详解
- C++中类对象的初始化与赋值的区别
- C++对象模型之函数成员(1)
- effective c++ 关于c++对象的初始化。
- 创建对象的工厂函数 c++11实现
- C++面向对象高级编程笔记03--GeekBand
- (Geekband)C++面向对象高级编程(第一周)
- C++中函数指针和函数对象的总结
- C++面向对象高级编程(上)学习笔记
- Symbian编程总结-基础篇-活动对象正解(4)-异步函数的同步调用