banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【転載】Go言語におけるシングルトンパターン

title: 【転載】Go 言語におけるシングルトンパターン
date: 2021-08-09 16:35:33
comment: false
toc: true
category:

  • Golang
    tags:
  • 転載
  • Go
  • 設計
  • パターン
  • シングルトン
  • エラー

この記事は転載です:Go 言語におけるシングルトンパターン | 李文周のブログ


過去数年間、Go 言語の発展は驚くべきものであり、他の言語(Python、PHP、Ruby)から Go 言語に移行するクロスランゲージ学習者を多く引き付けました。 Go 言語は並行処理を実現するのが非常に簡単であるため、多くの場面で誤って使用されています。

Go 言語におけるシングルトンパターン#

過去数年間、Go 言語の発展は驚くべきものであり、他の言語(Python、PHP、Ruby)から Go 言語に移行するクロスランゲージ学習者を多く引き付けました。

長い間、多くの開発者やスタートアップは Python、PHP、または Ruby を使用して強力なシステムを迅速に開発することに慣れており、ほとんどの場合、内部のトランザクションがどのように機能するか、スレッドセーフ性や並行性について心配する必要はありませんでした。最近数年で、マルチスレッドの高並行システムが流行し始め、私たちは強力なシステムを迅速に開発するだけでなく、開発されたシステムが十分に迅速に実行できることを保証する必要があります。(本当に大変です☺️)

Go 言語が生まれつき並行処理をサポートしている特性に惹かれたクロスランゲージ学習者にとって、Go 言語の文法を習得することは最も難しいことではないと思います。最も難しいのは、既存の思考の枠を打破し、並行処理を真に理解し、実際の問題を解決するために並行処理を使用することです。

Go 言語は並行処理を実現するのが非常に簡単であるため、多くの場面で誤って使用されています。

よくあるエラー#

並行性の安全性を考慮しないシングルトンパターンのようなよくあるエラーがあります。以下のサンプルコードのように:

package singleton

type singleton struct {}

var instance *singleton

func GetInstance() *singleton {
	if instance == nil {
		instance = &singleton{}   // 並行性安全ではない
	}
	return instance
}

上記の状況では、複数の goroutine が最初のチェックを実行でき、それらはすべてsingleton型のインスタンスを作成し、互いに上書きします。ここでどのインスタンスが返されるかを保証することはできず、そのインスタンスに対する他の操作は開発者の期待と一致しない可能性があります。

悪い理由は、シングルトンインスタンスへの参照を保持しているコードがある場合、異なる状態を持つこの型の複数のインスタンスが存在する可能性があり、潜在的に異なるコードの動作を引き起こすことです。これはデバッグプロセスの悪夢となり、このエラーを見つけるのが難しくなります。なぜなら、デバッグ中にランタイムが一時停止してもエラーが発生しないため、非並行性安全な実行の可能性が最小限に抑えられ、開発者の問題が隠されやすくなるからです。

過激なロック#

このような並行性の安全性の問題に対する悪い解決策もたくさんあります。以下のコードを使用すると、確かに並行性の安全性の問題を解決できますが、他の潜在的な深刻な問題を引き起こします。ロックを使用してこの関数への並行呼び出しを直列化しています。

var mu Sync.Mutex

func GetInstance() *singleton {
    mu.Lock()                    // インスタンスが存在する場合はロックする必要はない
    defer mu.Unlock()

    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

上記のコードでは、シングルトンインスタンスを作成する前にSync.Mutexを導入してロックを取得することで並行性の安全性の問題を解決していることがわかります。問題は、ここで過剰なロックを実行していることです。インスタンスがすでに作成されている場合は、単にキャッシュされたシングルトンインスタンスを返すべきです。高度に並行したコードの基盤では、同時に 1 つの goroutine しかシングルトンインスタンスを取得できないため、ボトルネックが発生する可能性があります。

したがって、これは最良の方法ではありません。他の解決策を考慮する必要があります。

チェック・ロック・チェックパターン#

C++ や他の言語では、最小限のロックを確保し、なおかつ並行性の安全性を保つ最良かつ最も安全な方法は、ロックを取得する際に広く知られているチェック・ロック・チェックパターンを利用することです。このパターンの擬似コードは以下のようになります。

if check() {
    lock() {
        if check() {
            // ここでロック安全なコードを実行
        }
    }
}

このパターンの背後にある考え方は、最初にチェックを行い、任意のアクティブロックを最小化するべきであるということです。なぜなら、IF 文のオーバーヘッドはロックよりも小さいからです。次に、ミューテックスを待って取得したいので、そのブロック内で同時に 1 つの実行のみが行われます。しかし、最初のチェックとミューテックスの取得の間に、他の goroutine がロックを取得する可能性があるため、インスタンスが別のインスタンスで上書きされないように、ロックの内部で再度チェックする必要があります。

このパターンをGetInstance()メソッドに適用すると、以下のようなコードになります:

func GetInstance() *singleton {
    if instance == nil {     // 完全に原子的ではないため、あまり完璧ではない
        mu.Lock()
        defer mu.Unlock()

        if instance == nil {
            instance = &singleton{}
        }
    }
    return instance
}

sync/atomicパッケージを使用することで、インスタンスが初期化されているかどうかを示すフラグを原子的に読み込み、設定できます。

import "sync"
import "sync/atomic"

var initialized uint32
... // ここは省略

func GetInstance() *singleton {

    if atomic.LoadUInt32(&initialized) == 1 {  // 原子操作 
		    return instance
	  }

    mu.Lock()
    defer mu.Unlock()

    if initialized == 0 {
         instance = &singleton{}
         atomic.StoreUint32(&initialized, 1)
    }

    return instance
}

しかし…… これは少し面倒に見えます。実際には、Go 言語と標準ライブラリがどのように goroutine の同期を実現しているかを研究することで、より良い方法があるかもしれません。

Go 言語の慣用的なシングルトンパターン#

私たちは Go の慣用的な方法を利用してこのシングルトンパターンを実現したいと考えています。標準ライブラリsyncの中にOnce型を見つけました。これは、ある操作が正確に 1 回だけ実行されることを保証します。以下は Go 標準ライブラリのソースコードの一部です(コメントの一部は削除されています)。

// Onceは正確に1つのアクションを実行するオブジェクトです。
type Once struct {
	// doneはアクションが実行されたかどうかを示します。
	// ホットパスで使用されるため、構造体の最初に配置されています。
	// ホットパスはすべての呼び出しサイトでインライン化されます。
	// doneを最初に配置することで、一部のアーキテクチャ(amd64/x86)ではよりコンパクトな命令が可能になり、
	// 他のアーキテクチャではオフセットを計算するための命令が少なくなります。
	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()
	}
}

これにより、特定の関数 / メソッドを 1 回だけ実行することができることが示されています。once.Do()の使い方は以下の通りです:

once.Do(func() {
    // ここで安全な初期化を実行
})

以下はシングルトン実装の完全なコードであり、この実装はsync.Once型を使用してGetInstance()へのアクセスを同期し、私たちの型が一度だけ初期化されることを保証します。

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

したがって、sync.Onceパッケージを使用することは、この目標を安全に達成するための最良の方法であり、Objective-C や Swift(Cocoa)がdispatch_onceメソッドを使用して類似の初期化を実行するのに似ています。

結論#

並行性や並列コードに関しては、コードをより注意深くチェックする必要があります。常にチームメンバーにコードレビューを実施させてください。このようなことは簡単に見つかります。

Go 言語に新しく移行したすべての開発者は、並行性の安全性がどのように機能するかを真に理解し、コードをより良く改善する必要があります。Go 言語自体は、並行性の知識がほとんどない状態で並行コードを設計できるように多くの重労働を行っていますが、特定の状況では、言語の特性に単純に依存するだけでは不十分であり、コードを開発する際にはベストプラクティスを適用する必要があります。

翻訳元http://marcio.io/2015/07/singleton-pattern-in-go/であり、可読性のために一部内容が修正されています。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。