Golang singleton pattern y sync.Once

Golang singleton pattern y sync.Once

antecedentes

Se puede decir que el patrón singleton es uno de los patrones de diseño más simples, y su función es muy simple: un tipo de cosa se instancia solo una vez, solo hay una instancia globalmente y se proporciona un método para obtener la instancia.

En Golang, el efecto de que una variable o una instancia se inicializa solo una vez initse puede lograr a través de una función. Cuando se importa un paquete, la función se ejecutará una vez inity no importa cuántas veces se importe el mismo paquete, solo se ejecutado una vez.

Sin embargo, el patrón singleton que este artículo principalmente quiere discutir es inicializar cuando se necesita por primera vez, es decir, inicialización diferida.

No tan buena implementación singleton

// bad_singleton.go


package main




import (
"sync"
)




var svcMu sync.Mutex
var svc *Svc




type Svc struct {
Num int
}




func GetSvc() *Svc {
if svc == nil { // 这一步判断不是并发安全的
svcMu.Lock()
defer svcMu.Unlock()
if svc == nil {
svc = &Svc{Num: 1}
svc = &Svc{}
svc.Num = 1



    }
}

return svc




}复制代码

}

svcMu.Lock()Tenga en cuenta que la declaración antes de ejecutar el mutex if svc == nil no es segura al mismo tiempo, es decir, en el escenario de múltiples goroutines llamando al mismo tiempo, uno de los goroutines está svcen proceso de inicializar la variable, y otros goroutines aquí juzgan que el resultado svcno es igual a mismo nilNo significa que la svcinicialización debe completarse.

Porque en ausencia de una sincronización explícita, el compilador y la CPU son libres de reorganizar el orden de las instrucciones que acceden a la memoria sobre la base de garantizar la coherencia serial dentro de cada gorutina.

Por ejemplo svc = &Svc{Num: 1}, esta línea parece ser solo una declaración de ejecución, y una implementación después de la reorganización puede tener el siguiente aspecto:

svc = &Svc{}
svc.Num = 1复制代码

Se puede ver que nilno significa que la inicialización deba completarse, por lo que el ejemplo anterior no es una implementación de singleton muy buena.

Mejor implementación de singleton

// good_singleton.go




package main




import (
"sync"
)




var svcOnce sync.Once
var svc *Svc




type Svc struct {
Num int
}




func GetSvc() *Svc {
svcOnce.Do(func() {
svc = &Svc{Num: 1}
})



return svc




}复制代码

}

sync.OnceEl Dométodo provisto solo ejecuta la función entrante una vez, sin importar cuántas veces se llame, entonces, ¿por qué es una mejor práctica usar el Dométodo para realizar la inicialización en lugar de una capa ?if svc == nil sync.Once

// sync.Once 源码




package sync




import (
"sync/atomic"
)




type Once struct {
done uint32
m    Mutex
}




func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // 这步是判断是否已经完成初始化的关键
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()
}
}复制代码

func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }

La implementación oficial sync.Oncees muy breve y concisa. Entre ellos atomic.LoadUint32(&o.done) == 0está el paso clave.Aquí se utiliza la declaración de operación atómica, que garantiza que sea seguro incluso en escenarios concurrentes, y que la lectura y escritura de datos estén completas.

o.done的值为0时表示未进行初始化或正在初始化中,只有等于1时才表示初始化已经完成,即f()执行完成后由defer atomic.StoreUint32(&o.done, 1)语句给o.done赋值1;也就是o.done作为是否完成初始化的标识,可能的值只有前面说的两个,为0时则加锁并尝试初始化流程,反之则视为已完成初始化直接跳过,这样就完美兼顾了效率与并发安全。

由此可见sync.Once内置的初始化完成标识判断远比if svc == nil 靠谱,因此像上面这样使用sync.Once实现单例模式是最推荐的方式。

额外推荐

实则开发中用到的设计模式经常不止一种,越是复杂大型的项目就越需要使用更多合适的模式来优化代码。

下面要推荐的是RefactoringGuru。这是我所见过最好的设计模式教程,是国外创建的一个教程网站,有中文站点,图文并茂地介绍每一种模式的结构、关系和逻辑,
最重要的是示例代码方面囊括了常见的几种主流编程语言,是个适合多数程序员学习设计模式的好地方!

下图是设计模式的目录页面(是不是很图文并茂呢):

结语

以上为本人学习和实践的一些总结,如有错漏还请不吝赐教。

参考

《Go程序设计语言》9.5 延迟初始化:sync.Once 网络版
Go 单例模式讲解和代码示例

本文来源: Golang 单例模式与sync.Once

Supongo que te gusta

Origin juejin.im/post/7150321573314969613
Recomendado
Clasificación