参考资料
[1] 慕雪年华:C++11中局部static变量的线程安全问题
[2] open-std:cpp 标准草案,第 6.7 节
在 C++ 工程实践中,接触到一个单例模式的实现,比较有意思。直接贴出来:
class A {
private:
// private constructor
A() = default;
// delete all copy/move constructor and assignment
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
public:
static inline A *GetInstance() {
static A a;
return &a;
}
};将构造函数声明为 private,并删除拷贝/移动构造和赋值,是单例的基础操作。
巧妙之处在于,这个单例模式利用了[1]和[2]中提到的特性。当一个局部静态变量被初始化时,多线程的执行环境应当被阻塞,直到变量初始化完成。
Otherwise 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.
编译器一般用互斥锁来实现这种“懒”构造的线程安全性。我们试着编译如下程序:
// main.cpp
#include <iostream>
void somefun() {
// pretend to do something...
}
class A {
private:
// private constructor
A() {
somefun();
}
// delete all copy/move constructor and assignment
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
public:
static inline A *GetInstance() {
static A a;
return &a;
}
void HelloWorld() const {
std::cout << "Hello, World!\n";
}
};
int main() {
A::GetInstance()->HelloWorld();
return 0;
}利用编译命令:g++ main.cpp -std=c++11 -O0 进行编译,得到 a.out 文件。
我们引入 somefun 的原因是想避免 class A 成为纯值类型,导致不用特别调用构造函数(也就不用加锁)。
我们利用 objdump -d a.out 查看汇编代码,找到 A::GetInstance 的函数体:
00000000000011c8 <_ZN1A11GetInstanceEv>:
11c8: f3 0f 1e fa endbr64
11cc: 55 push %rbp
11cd: 48 89 e5 mov %rsp,%rbp
11d0: 0f b6 05 81 2f 00 00 movzbl 0x2f81(%rip),%eax # 4158 <_ZGVZN1A11GetInstanceEvE1a>
11d7: 84 c0 test %al,%al
11d9: 0f 94 c0 sete %al
11dc: 84 c0 test %al,%al
11de: 74 36 je 1216 <_ZN1A11GetInstanceEv+0x4e>
11e0: 48 8d 05 71 2f 00 00 lea 0x2f71(%rip),%rax # 4158 <_ZGVZN1A11GetInstanceEvE1a>
11e7: 48 89 c7 mov %rax,%rdi
11ea: e8 a1 fe ff ff call 1090 <__cxa_guard_acquire@plt>
11ef: 85 c0 test %eax,%eax
11f1: 0f 95 c0 setne %al
11f4: 84 c0 test %al,%al
11f6: 74 1e je 1216 <_ZN1A11GetInstanceEv+0x4e>
11f8: 48 8d 05 52 2f 00 00 lea 0x2f52(%rip),%rax # 4151 <_ZZN1A11GetInstanceEvE1a>
11ff: 48 89 c7 mov %rax,%rdi
1202: e8 a9 ff ff ff call 11b0 <_ZN1AC1Ev>
1207: 48 8d 05 4a 2f 00 00 lea 0x2f4a(%rip),%rax # 4158 <_ZGVZN1A11GetInstanceEvE1a>
120e: 48 89 c7 mov %rax,%rdi
1211: e8 5a fe ff ff call 1070 <__cxa_guard_release@plt>
1216: 48 8d 05 34 2f 00 00 lea 0x2f34(%rip),%rax # 4151 <_ZZN1A11GetInstanceEvE1a>
121d: 5d pop %rbp
121e: c3 ret
121f: 90 nop可以看到,在地址 11ea 和 1211 处的语句是加锁解锁的函数调用。中间则为对 class A 构造函数的调用。
笔者测试的平台为 amd64。大约在其他平台上,也是相似的情况吧。
11ea: e8 a1 fe ff ff call 1090 <__cxa_guard_acquire@plt>
...
1211: e8 5a fe ff ff call 1070 <__cxa_guard_release@plt>