C++11中局部static变量的线程安全问题
前言
大家都知道,在C++11标准中,要求局部静态变量初始化具有线程安全性,所以我们可以很容易实现一个线程安全的单例类:
class Foo
{
public:
static Foo *getInstance()
{
static Foo s_instance;
return &s_instance;
}
private:
Foo() {}
};
在C++标准中,是这样描述的(在标准草案的6.7节中):
such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.
分析
标准关于局部静态变量初始化,有这么几点要求:
- 变量在代码第一次执行到变量声明的地方时初始化。
- 初始化过程中发生异常的话视为未完成初始化,未完成初始化的话,需要下次有代码执行到相同位置时再次初始化。
- 在当前线程执行到需要初始化变量的地方时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。
- 如果初始化过程中发生了对初始化的递归调用,则视为未定义行为。
关于第4点,如果不明白,可以参考以下代码:
class Bar
{
public:
static Bar *getInstance()
{
static Bar s_instance;
return &s_instance;
}
private:
Bar()
{
getInstance();
}
};
GCC的实现
以GCC 7.3.0版本为例,我们来分析GCC是如何实现标准的。
Foo::getInstance()
使用GCC编译后,我们使用gdb将文章开头的Foo::getInstance()
反汇编:
Dump of assembler code for function Foo::getInstance():
0x00005555555546ea <+0>: push %rbp
0x00005555555546eb <+1>: mov %rsp,%rbp
=> 0x00005555555546ee <+4>: movzbl 0x20092b(%rip),%eax # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
0x00005555555546f5 <+11>: test %al,%al
0x00005555555546f7 <+13>: sete %al
0x00005555555546fa <+16>: test %al,%al
0x00005555555546fc <+18>: je 0x55555555472b <Foo::getInstance()+65>
0x00005555555546fe <+20>: lea 0x20091b(%rip),%rdi # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
0x0000555555554705 <+27>: callq 0x5555555545b0 <__cxa_guard_acquire@plt>
0x000055555555470a <+32>: test %eax,%eax
0x000055555555470c <+34>: setne %al
0x000055555555470f <+37>: test %al,%al
0x0000555555554711 <+39>: je 0x55555555472b <Foo::getInstance()+65>
0x0000555555554713 <+41>: lea 0x2008fe(%rip),%rdi # 0x555555755018 <_ZZN3Foo11getInstanceEvE10s_instance>
0x000055555555471a <+48>: callq 0x555555554734 <Foo::Foo()>
0x000055555555471f <+53>: lea 0x2008fa(%rip),%rdi # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
0x0000555555554726 <+60>: callq 0x5555555545a0 <__cxa_guard_release@plt>
0x000055555555472b <+65>: lea 0x2008e6(%rip),%rax # 0x555555755018 <_ZZN3Foo11getInstanceEvE10s_instance>
0x0000555555554732 <+72>: pop %rbp
0x0000555555554733 <+73>: retq
End of assembler dump.
在+4
、+20
、+53
出现的_ZGVZN3Foo11getInstanceEvE10s_instance
使用c++filt
分析为guard variable for Foo::getInstance()::s_instance
,而+41
、+65
位置出现的_ZZN3Foo11getInstanceEvE10s_instance
则为Foo::getInstance()::s_instance
。后者是s_instance
这个局部静态变量,前者从名字看就知道是个guard
标志变量,用来指示局部静态变量的初始化状态。
+4 ~ +18
测试guard
变量的第一个字节,如果为0
,代表s_instance
未初始化,进入+27
;否则代表s_instance
已初始化,进入+65
。
+20 ~ +27
以guard
变量地址作为参数,执行__cxa_guard_acquire
函数。
+32 ~ +39
测试返回值,如果为0
,代表s_instance
已初始化,进入+65
;否则代表s_instance
未初始化,进入+41
。
+41 ~ +48
初始化s_instance
+53 ~ +60
以guard
变量地址作为参数,执行__cxa_guard_release
函数。
+65 ~ +73
返回s_instance
地址
__cxa_guard_acquire
我们来看看__cxa_guard_acquire
这个函数具体做了什么,该函数代码位于gcc-7-7.3.0/gcc-7.3.0/libstdc++-v3/libsupc++/guard.cc
。由于这个函数针对不同平台做了不同的实现,有些我们不需要的代码,以我机器的设置,支持线程和futex系统调用,所以删除了一些不相关的代码:
1 | int __cxa_guard_acquire (__guard *g) |
- 首先检测
guard
变量,guard
变量等于1
的话,直接返回0
,代表s_instance
已初始化,不需要再次初始化。 - 检测是否为多线程环境,如果没有多线程的话,也就没有必要去做额外工作来保证线程安全了。
guard_bit
表示s_instance
已经初始化成功;pending_bit
表示s_instance
正在初始化;waiting_bit
表示有其他线程正在等待s_instance
的初始化。- 使用一个原子操作来检测
guard
变量是否为0
,如果为0
,则由当前线程初始化s_instance
,把pending_bit
写入guard变量,返回1
。如果不为0
,则将guard当前值写入expected
。 - 检测
expected
值是否为guard_bit
,如果是,则s_instance
已初始化完成,不再需要初始化,返回0
。 - 检测
expected
值是否为pending_bit
,如果是,说明s_instance
正在初始化,且没有其他线程等待初始化。 - 将
newv
变量设置为pending_bit | waiting_bit
,表示s_instance
正在初始化且有线程正在等待初始化。 - 使用一个原子操作来检测
guard
变量是否为pending_bit
,如果不是,说明有其他线程修改了guard
变量,需要做进一步检测;如果是,说明没有其他线程修改guard
变量,则将pending_bit | waiting_bit
写入guard
变量。 - 如果
expected
等于guard_bit
,说明s_instance
被初始化成功,不需要再初始化,返回0
。 - 如果
expected
等于0
,说明s_instance
初始化失败,回到4
重新开始检测。 - 如果在
8
中没有其他线程修改过guard
变量,将expected
设置为pending_bit | waiting_bit
,表示s_instance
正在初始化且有线程(也就是当前线程)正在等待初始化。 - 如果在
6
处没有进入if分支,说明expected
等于pending_bit | waiting_bit
,如果进入了if分支,由11
可得,此时expected
也被修改为了pending_bit | waiting_bit
。总之,此时s_instance
正在初始化且有线程正在等待初始化。利用futex
系统调用,再次检测guard
变量是否发生了变化,如果发生了变化,回到4
重新开始检测;如果没有发生变化,仍然等于pending_bit | waiting_bit
,则挂起当前线程。
总之,__cxa_guard_acquire
要么返回0
要么返回1
,用来指示s_instance
已初始化或未初始化。__cxa_guard_acquire
可能会导致当前线程挂起,这发生在s_instance
正在初始化的时候。
__cxa_guard_release
由于__cxa_guard_acquire
可能导致当前线程挂起,因此需要在s_instance
初始化完成后使用将__cxa_guard_release
线程唤醒。
1 | void __cxa_guard_release (__guard *g) throw () |
- 检测是否为多线程环境
- 使用原子操作将
guard
变量置为guard_bit
,同时获取guard
变量原始值。 - 如果
guard
变量原始值包含waiting_bit
,说明有线程挂起(或将要调用futex
欲使线程挂起),调用futex
唤醒挂起的进程。
__cxa_guard_abort
由于s_instance
可能初始化失败(本例中并未体现),因此还有一个__cxa_guard_abort
函数。
1 | void __cxa_guard_abort (__guard *g) throw () |
与__cxa_guard_release
基本一致,不同的地方在于会将guard
变量置0
。
递归初始化调用
由于在C++11标准中,初始化如果发生了递归是未定义行为,所以GCC 7.3.0针对是否为多线程环境做了不同的处理。如果是多线程环境,不进行额外处理,会发生死锁;如果是单线程环境,则会抛异常。
1 | // acquire() is a helper function used to acquire guard if thread support is |
总结
看到了GCC如此复杂的实现,我的个人感想是还是不要自己造轮子来保证单例类的线程安全了,想要做到和GCC一样的高效还是比较难的,利用C++11标准的带来的便利就挺好。
1 | 原文作者:imred |