当前位置: 首页 > article >正文

单例模式何以保证线程安全

接上一篇【汇编下的单例模式】,今天来分析下为什么局部静态变量实现的单例模式是《线程安全》的。

汇编层:

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


http://www.kler.cn/a/7648.html

相关文章:

  • 全面解读 USB Key:定义、使用场景、加密技术及 Java 实现
  • 深度神经网络DNN反向传播BP算法公式推导
  • NodeJS 百度智能云文本转语音(实测)
  • gitHub常用操作
  • 报错 No available slot found for the embedding model
  • SpringCloud基础 入门级 学习SpringCloud 超详细(简单通俗易懂)
  • Less 运行环境
  • ChatGPT能够干翻谷歌吗?
  • 蓝桥杯备考
  • 【Python】如何实现Redis构造简易客户端(教程在这)
  • 学习 Python 之 Pygame 开发魂斗罗(十四)
  • Visual Studio Code 1.77 发布,扩展的 GitHub Copilot 集成
  • ArduPilot飞控之DIY-F450计划
  • JayDeBeApi对数据类型的支持
  • Linux- 系统随你玩之--玩出花活的命令浏览器上
  • 360周鸿祎离婚老婆能分得90亿,如果奶茶妹妹离婚会不会分走更多?
  • 7-6 莫比乌斯最大值isUsefulAlgorithm(2023郑州轻工业大学校赛
  • 【论文阅读】如何给模型加入先验知识
  • 本地生活服务,快手直播电商外的又一大金矿!
  • 集成华为运动健康服务干货总览
  • 不敲代码用ChatGPT开发一个App
  • ABC206F Interval Game 2
  • python实现一个创建日志收集器代码
  • 智慧水务信息化平台建设,实现供水一体化管控
  • 技术分享| 什么是动态更新?
  • 自动化篇 | 13 | app自动化:airtest