• 【Go】单例模式与Once源码


    单例实现

    type singleton struct{}
    
    var (
    	instance    *singleton
    	initialized uint32
    	mu          sync.Mutex
    )
    
    func Instance() *singleton {
    	if atomic.LoadUint32(&initialized) == 1 {
    		return instance
    	}
    
    	mu.Lock()
    	defer mu.Unlock()
    
    	if instance == nil {
    		defer atomic.StoreUint32(&initialized, 1)
    		instance = &singleton{}
    	}
    	return instance
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    其中通用的代码提取出来,就成了标准库中sync.Once的实现:

    type Once struct {
    	done uint32
    	m    sync.Mutex
    }
    
    func (o *Once) Do(f func()) {
    	if atomic.LoadUint32(&o.done) == 0 {
    
    		o.m.Lock()
    		defer o.m.Unlock()
    
    		if o.done == 0 {
    			defer atomic.StoreUint32(&o.done, 1)
    			f()
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    于是,使用sync.Once重新实现单例模式

    var (
    	instance2 *singleton
    	once sync.Once
    )
    
    func Instance2() *singleton {
    	once.Do(func() {
    		instance2 = &singleton{}
    	})
    	return instance2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    sync.Once源码分析

    1. lock并不会同步值

    在lock和unlock之间修改值,并不会保证对其他协程是可见的,除非使用相同的Mutex加锁,想要同步值必须使用atomic;

    lock可以通过串行化,使得两个协程的操作存在happen-before关系,从而是的操作可见

    happen-before原则定义如下:

    如果一个操作happens-before(之前发生)另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

    两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

    2. Do执行一次

    当第一次执行完Do之后,done设置成1,后面执行Do会直接跳过

    3. Once执行Do后不准copy

    A Once must not be copied after first use.

    sync.Once执行完Dodone已经设置成1了,copy出来的once执行Do会直接跳过

    4. Do并发时阻塞

    当两个或者多个协程同时调用Do时,先调用的协程执行,后面的协程会阻塞;

    解释:以单例使用once的实现说明,两个协程同时调用Instance2(),先调用的协程执行创建并拿到返回值,后调用的阻塞,

    ​ 等到先调用的完成后再拿到返回值;

    意义:这样的好处是防止后调用的协程拿到的是nil

    源码说明:上面第二段代码13行使用defer,要等f()结束才会把done设置成1;其他协程并发调用Do时,done==0

    ​ 然后请求m.Lock()形成阻塞

    5. Do递归死锁

    如果Do中的方法调用当前once的Do会造成死锁,原因参考上面一点(sync.Mutex.Lock()时不可重入锁)

    参考

    • 《Go语言高级编程》
    • Go1.16源码
  • 相关阅读:
    前微软CEO的“离别礼物“:Cortana差点改名为“Bingo”
    git常用的几个命令
    2022.5.15-参加北京青少年程序设计展示活动海淀区赛(失误了,三等奖)
    ChatGLM3 本地部署的解决方案
    09、全文检索 -- Solr -- SpringBoot 整合 Spring Data Solr (生成DAO组件 和 实现自定义查询方法)
    【21天学习挑战】经典算法之【递归算法】
    Java 创建多线程
    接口测试之文件上传
    Zookeeper - 集群搭建
    ATFX汇市:英国通胀率大降两个百分点,GBPUSD止步近两月高点
  • 原文地址:https://blog.csdn.net/qq_22038259/article/details/128123515