本文最后更新于:2022年10月18日 下午
起因是想实现一个单例模式的类,说起单例模式,那肯定逃不开拷贝控制和线程安全问题,因为项目需求,我选择使用饿汉式方法实现。饿汉式在网上的主流说法都是“不需要加锁,执行效率高,线程安全的”“不管是不是用都会初始化,浪费内存”这两点。然后看到了面试中的 Singleton 这篇文章,里面提到了
“好的。首先,我使用了一个指针记录创建的 Singleton 实例,而不再是局部静态变量。这是因为局部静态变量可能在多线程环境下出现问题。”
“我想插一句话,为什么局部静态变量会在多线程环境下出现问题?”
“这是由局部静态变量的实际实现所决定的。为了能满足局部静态变量只被初始化一次的需求,很多编译器会通过一个全局的标志位记录该静态变量是否已经被初始化的信息。那么,对静态变量进行初始化的伪码就变成下面这个样子…
我产生了疑惑,于是决定探究一下。
返回实例的函数如下:
static T& GetInstance () { static T instance; return instance; }
使用 MSVC 编译后,Debug 模式下的汇编如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static T& GetInstance() { 00007FF6A8CA2F60 push rbp 00007FF6A8CA2F62 push rdi 00007FF6A8CA2F63 sub rsp,0E8h 00007FF6A8CA2F6A lea rbp,[rsp+20h] 00007FF6A8CA2F6F lea rcx,[__5065BBA5_Singleton@hpp (07FF6A8F5609Ch)] 00007FF6A8CA2F76 call __CheckForDebuggerJustMyCode (07FF6A8C7F845h) static T instance; 00007FF6A8CA2F7B mov eax,11Ch 00007FF6A8CA2F80 mov eax,eax 00007FF6A8CA2F82 mov ecx,dword ptr [_tls_index (07FF6A8E9BB88h)] 00007FF6A8CA2F88 mov rdx,qword ptr gs:[58h] 00007FF6A8CA2F91 mov rcx,qword ptr [rdx+rcx*8] 00007FF6A8CA2F95 mov eax,dword ptr [rax+rcx] 00007FF6A8CA2F98 cmp dword ptr [$TSS0 (07FF6A8E91350h)],eax 00007FF6A8CA2F9E jle Singleton<RenderWork>::GetInstance+79h (07FF6A8CA2FD9h) 00007FF6A8CA2FA0 lea rcx,[$TSS0 (07FF6A8E91350h)] 00007FF6A8CA2FA7 call _Init_thread_header (07FF6A8C7F241h) 00007FF6A8CA2FAC cmp dword ptr [$TSS0 (07FF6A8E91350h)],0FFFFFFFFh 00007FF6A8CA2FB3 jne Singleton<RenderWork>::GetInstance+79h (07FF6A8CA2FD9h) 00007FF6A8CA2FB5 lea rcx,[instance (07FF6A8E91290h)] 00007FF6A8CA2FBC call RenderWork::RenderWork (07FF6A8C7F917h) 00007FF6A8CA2FC1 lea rcx,[`Singleton<RenderWork>::GetInstance'::`2'::`dynamic atexit destructor for 'instance'' (07FF6A8C7E260h)] 00007FF6A8CA2FC8 call atexit (07FF6A8C7D608h) 00007FF6A8CA2FCD lea rcx,[$TSS0 (07FF6A8E91350h)] 00007FF6A8CA2FD4 call _Init_thread_footer (07FF6A8C7F3C2h) return instance; 00007FF6A8CA2FD9 lea rax,[instance (07FF6A8E91290h)] } 00007FF6A8CA2FE0 lea rsp,[rbp+0C8h] 00007FF6A8CA2FE7 pop rdi 00007FF6A8CA2FE8 pop rbp 00007FF6A8CA2FE9 ret
这里初始化的变量属于继承了Singleton<RenderWork>
的派生类。
00007FF6A8CA2F98 cmp dword ptr [$TSS0 (07FF6A8E91350h)],eax 00007FF6A8CA2F9E jle Singleton<RenderWork>::GetInstance+79h (07FF6A8CA2FD9h)
这两行比较是否已经初始化了对象,如果已经初始化则跳转到 return 那行(07FF6A8CA2FD9h),否则进行变量初始化。
在初始化前,他调用了_Init_thread_header
这个函数,单步调试跟进去发现这个函数在thread_safe_statics.cpp
这个文件中,看文件名就知道了,MSVC
编译器处理了静态变量初始化的线程安全问题,具体方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 extern "C" void __cdecl _Init_thread_header(int * const pOnce) noexcept { _Init_thread_lock(); if (*pOnce == uninitialized) { *pOnce = being_initialized; } else { while (*pOnce == being_initialized) { _Init_thread_wait(xp_timeout); if (*pOnce == uninitialized) { *pOnce = being_initialized; _Init_thread_unlock(); return ; } } _Init_thread_epoch = _Init_global_epoch; } _Init_thread_unlock(); }
前面的注释说:“初始化表达式的控制访问。在完成变量初始化之前仅一个线程可以离开这个函数,这个线程将执行初始化。所有其他线程将会阻塞直到初始化完毕/失败。”。
这里的参数pOnce
也就是要初始化变量的指针,其值分为四种状态:
uninitialized(0) :变量未初始化 ,全局变量的初始值为 0、变量创建失败置为 0
being_initialized(-1) :变量即将初始化 ,代表当前线程可以初始化这个变量,这个值会被这个函数赋予
简单描述一下流程:
首先获得互斥锁,如果当前变量处于为未初始化状态(0)则置为即将初始化状态(-1)并解锁、返回函数准备进行变量初始化;
如果当前已经是即将初始化状态(-1),即已经有线程在初始化这个变量了,进入循环等待;
如果转换为未初始化状态(0)则表明变量在其他线程初始化失败了(new
失败将变量置为 0),此时将变量置为即将初始化的状态(-1)并解锁、返回函数准备进行变量初始化;
如果变量不再处于即将初始化状态(-1),则表示其他线程已经完成了变量初始化,解锁并退出循环。
函数返回后将会执行下列代码:
00007FF6A8CA2FAC cmp dword ptr [$TSS0 (07FF6A8E91350h)],0FFFFFFFFh 00007FF6A8CA2FB3 jne Singleton<RenderWork>::GetInstance+79h (07FF6A8CA2FD9h)
这两行将检查变量的状态,如果处于即将初始化的状态(-1)也就是0FFFFFFFFh
则进行变量初始化,否则跳转到 return。
这样就保证了静态变量初始化时的线程安全问题,至少在MSVC
下可以放心使用了。