C/C++编程教训----函数内静态类对象初始化非线程安全(C++11之前)
不少程序员在编写程序的时候,会使用函数内静态(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线程安全
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或者以上吧。
上一篇: 人生三大错觉之一
下一篇: 怀孕之后最重要的是什么