单例模式何以保证线程安全
接上一篇【汇编下的单例模式】,今天来分析下为什么局部静态变量实现的单例模式是《线程安全
》的。
汇编层:
A::getInstance(): # @A::getInstance()
push rbp
mov rbp, rsp
sub rsp, 16
cmp byte ptr [rip + guard variable for A::getInstance()::m_a], 0 # 因为静态变量存放在bss或者data段,因此可通过存放当前指令的rip寄存器加相对地址进行访问,(32位系统是eip寄存器)
jne .LBB1_4 # 不为0(即空值)时表示静态对象已生成,跳转到 .LBB1_4 标签处返回
lea rdi, [rip + guard variable for A::getInstance()::m_a] # 为0时将m_a的位置传到rdi寄存器中,后面生成对象的时候就以rdi的值作为起始地址
call __cxa_guard_acquire@PLT # __cxa_guard_acquire 和 下面的__cxa_atexit、__cxa_guard_release配套使用,__cxa_guard_acquire表示加锁进行初始化
cmp eax, 0
je .LBB1_4 # 为0表示初始化加锁失败,表示已有初始化在进行中,直接返回
lea rdi, [rip + A::getInstance()::m_a]
call A::A() [base object constructor] # 加锁成功,调用构造函数
jmp .LBB1_3
.LBB1_3:
lea rdi, [rip + A::~A() [base object destructor]]
lea rsi, [rip + A::getInstance()::m_a]
lea rdx, [rip + __dso_handle] # 将dso_handler析构器地址存入rdx寄存器中
call __cxa_atexit@PLT # 注册析构函数
lea rdi, [rip + guard variable for A::getInstance()::m_a]
call __cxa_guard_release@PLT # 释放锁
.LBB1_4:
lea rax, [rip + A::getInstance()::m_a]
add rsp, 16
pop rbp
ret # 返回静态对象,下面为初始化抛出异常的处理代码
mov rcx, rax
mov eax, edx
mov qword ptr [rbp - 8], rcx
mov dword ptr [rbp - 12], eax
lea rdi, [rip + guard variable for A::getInstance()::m_a]
call __cxa_guard_abort@PLT # 初始化失败,抛出异常
mov rdi, qword ptr [rbp - 8]
call _Unwind_Resume@PLT # 回退到初始化起始帧栈
可以看到在生成静态局部变量时会判断guard variable,先后call了__cxa_guard_acquire
、__cxa_atexit
、__cxa_guard_release
这三个函数,若初始化失败还会调用__cxa_guard_abort
函数,这4个函数都是编译器gcc中实现的,那么这几个函数具体怎么实现的线程安全呢?
gcc源码(点击下载):
extern "C"
int __cxa_guard_acquire (__guard *g) {
#ifdef _GLIBCXX_USE_FUTEX //futex是linux内核中锁机制,__atomic_相关的原子操作函数是Intel x86架构上独有的
// 所以在arm硬件平台上是不支持这些操作的,只能使用mutex进行加锁。
// If __atomic_* and futex syscall are supported, don't use any global mutex.
int *gi = (int *) (void *) g;
const int guard_bit = _GLIBCXX_GUARD_BIT; // 初始化完成
const int pending_bit = _GLIBCXX_GUARD_PENDING_BIT; // 正在初始化
const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT; // 等待初始化
while (1)
{
int expected(0);
if (__atomic_compare_exchange_n(gi, &expected, pending_bit, false,
__ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE))
{
// This thread should do the initialization.
return 1;
}
if (expected == guard_bit)
{
// Already initialized.
return 0;
}
if (expected == pending_bit)
{
// Use acquire here.
int newv = expected | waiting_bit;
if (!__atomic_compare_exchange_n(gi, &expected, newv, false,
__ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE))
{
if (expected == guard_bit)
{
// Make a thread that failed to set the waiting bit exit the function earlier,
// if it detects that another thread has successfully finished initialising.
return 0;
}
if (expected == 0)
continue;
}
expected = newv;
}
syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAIT, expected, 0);
#else //不适用原子操作,使用mutex加锁
if (__gthread_active_p ())
{
mutex_wrapper mw;
while (1) // When this loop is executing, mutex is locked.
{
// The static is already initialized.
if (_GLIBCXX_GUARD_TEST(g))
return 0; // The mutex will be unlocked via wrapper
if (init_in_progress_flag(g))
{
// The guarded static is currently being initialized by
// another thread, so we release mutex and wait for the
// condition variable. We will lock the mutex again after
// this.
get_static_cond().wait_recursive(&get_static_mutex());
}
else
{
set_init_in_progress_flag(g, 1);
return 1; // The mutex will be unlocked via wrapper.
}
}
}
#endif
}
从上面可以看到如果是在x86上支持原子操作,那么__cxa_guard_acquire
会调用__atomic_compare_exchange_n
函数进行CAS,对于以__atomic_*开头的函数,在GCC4.7.版本之前是以__sync_*开头的函数,他们之间主要的不同在于__atomic_系列的参数有内存序参数而__sync_系列没有,所以建议使用新的__atomic_系列函数。
对于__atomic_compare_exchange_n函数其中的__ATOMIC_ACQ_REL
内存序参数,有以下几种内存序:
__ATOMIC_RELAXED:最低约束等级,表示没有线程间排序约束
__ATOMIC_CONSUME:官方表示因为C++11的memory_order_consume语义不足,当前使用更强的__ATOMIC_ACQUIRE来实现。
__ATOMIC_ACQUIRE:对释放操作创建线程间happens-before限制,防止代码在操作前的意外hoisting
__ATOMIC_RELEASE:对获取操作创建线程间happens-before限制,防止代码在操作后的意外sinking
__ATOMIC_ACQ_REL:结合了前述两种限制
__ATOMIC_SEQ_CST:约束最强,默认
// 比较ptr和expected指向的内容,如果相等,则进行read-modify-write操作,将desired值写入ptr指向的内存中,内存序使用
// success_memorder;若不相等,则进行read操作并将*ptr的内容写到*expected中,内存序使用failure_memorder
// weak为true时表示操作允许失败,一般都是false。
bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak,
int success_memorder, int failure_memorder)
对于内存模型的种类及作用,这里不做过多的解释,后面另写一篇blog进行阐述。
总结
其实要实现简单的线程安全并不难,加个锁就能搞定,但是如果要实现高性能的线程安全,就需要考虑到不同编译器、不同系统架构上的特性进行定制化,能用原子操作的就用原子操作,自己造轮子还是有些勉强。
参考官网链接
https://wiki.osdev.org/C++#GCC
https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html