Golang シングルトン パターンと sync.Once
バックグラウンド
シングルトン パターンは、最も単純な設計パターンの 1 つと言えます。その機能は非常に単純です。つまり、ある型が 1 回だけインスタンス化され、インスタンスがグローバルに 1 つだけ存在し、インスタンスを取得するためのメソッドが提供されます。
Golang では、変数やインスタンスが一度だけ初期化されるという効果をinit
関数によって実現できます. パッケージがインポートされると、関数は 1 回実行されinit
、同じパッケージが何度インポートされても実行されるのは 1 回だけです.一度実行。
ただし、この記事で主に説明したいシングルトン パターンは、最初に必要になったときに初期化する、つまり遅延初期化です。
シングルトンの実装があまり良くない
// 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()
前のステートメントif svc == nil
は同時に安全ではないことに注意してください。つまり、複数のゴルーチンが同時に呼び出すシナリオでは、ゴルーチンの 1 つがsvc
変数の初期化の過程にあり、ここで他のゴルーチンは結果がsvc
等しくないと判断します初期化を完了しなければならnil
ないという意味ではありません。svc
明示的な同期がない場合、コンパイラと CPU は、各ゴルーチン内でシリアルの一貫性を確保することに基づいて、メモリにアクセスする命令の順序を自由に再配置できるためです。
たとえばsvc = &Svc{Num: 1}
、この行は単なる実行ステートメントのようであり、再配置後の実装は次のようになります。
svc = &Svc{}
svc.Num = 1复制代码
初期化を完了しなければならないという意味ではないことがわかるnil
ので、上記の例はあまり良いシングルトン実装ではありません。
より良いシングルトン実装
// 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.Once
提供されDo
たメソッドは、何回呼び出されても着信関数を 1 回しか実行しないのにDo
、レイヤーの代わりにメソッドを使用して初期化を実行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()
}
}
公式のsync.Once
実装は非常に短く簡潔です。その中atomic.LoadUint32(&o.done) == 0
で重要なステップは、ここでアトミック操作ステートメントが使用されているため、並行シナリオでも安全であり、データの読み取りと書き込みが完了します。
当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