sync.Once——golang单例模式

sync.Once——golang单例模式,第1张

之前写的一个java的单例模式

在golang中我们期望一个方法只执行一次的时候,通常会使用实例化一个sync.Once结构体,然后使用once.Do方法来包装我们需要执行的方法f。在初始化各种连接的时候非常常用。

代码示例

import (
	"sync"
)

// 单例对象的结构体
type Singleton struct {
	Age  int
	Name string
	Addr string
}

var once sync.Once

// 全局变量instance
var instance *Singleton

// 执行方法
func GetInstance() *Singleton {
	if instance == nil {
		once.Do(func() {
			instance = &Singleton{
				Age:  20,
				Name: "张三",
				Addr: "余杭区",
			}
		})
	}
	return instance
}

上述代码的 if instance == nil其实是非必要的,只是让逻辑看起来更加清晰。

源码剖析
once的内部实现非常简单,无异于我们自己实现的单例模式,先来看Once结构体的样子

// Once is an object that will perform exactly one action.
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

结构体中包含一个标识位done和一个锁m。m没啥说的就是锁,done用来判断方法是否已经执行过,初始状态为未执行。注意这里注释提到了一个hot path,大意为使用频次非常高的指令序列。在获取结构体中的第一个字段的时候,可以直接取消引用结构体指针。而要获取后面的字段则需要加上偏移量。所以把done放在第一个字段可以提高性能。

再来看Do方法的实现

func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.

	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}
func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

在Do方法中先进行了atomic.LoadUint32(&o.done) == 0的判断,如果标识位已经改变了则意味着f已经执行过了。doSlow中使用锁保证只能有一个线程执行这个方法,在锁的临界区内又进行了一次o.done==0的判断,就是双重检查 防止等待锁的线程获得锁之后又执行了一次f(因为是在锁的临界区内,所以不需要使用atomic,直接判断即可)。

注释里提到了一个常见的错误实现

	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
		f()
	}

这种方式不能够保证标志位改变时方法f一定执行完毕。这可能会导致外层方法判断f方法已经执行完毕但实际上并没有,造成各种未知错误(如空指针)。

注意点,要执行的方法f中不能够再次调用do,这样会导致死锁。doSlow中获取了锁,doSlow又调用了f,f又去调用doSlow获取锁。

PS:golang中没有实现可重入锁,设计者认为锁的使用应该是清晰有层次的,如果出现需要可重入锁的情况,那么你应该要修改你的代码了。

PPS:如果方法f在执行过程中发生了panic,也会视为执行完成,后续不会再执行f,这一点需要特殊注意。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/langs/720512.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存