sync.Once: より安全な並行処理のためのGoのシンプルなパターン
Grace Collins
Solutions Engineer · Leapcell

🔍 Go concurrencyの本質:sync.Onceファミリの包括的なガイド
Goの並行プログラミングでは、ある操作が一度だけ実行されるようにすることがよくあります。標準ライブラリの軽量な同期プリミティブとして、sync.Onceはこの問題を非常にシンプルな設計で解決します。この記事では、この強力なツールの使い方と原則を深く理解することができます。
🎯 sync.Onceとは?
sync.Onceは、Go言語のsyncパッケージの同期プリミティブです。そのコア機能は、プログラムのライフサイクル中に特定の操作が一度だけ実行されることを保証することです。これは、同時にそれを呼び出すゴルーチンがいくつあっても関係ありません。
公式の定義は簡潔で強力です。
Onceは、特定の操作が一度だけ実行されることを保証するオブジェクトです。 Onceオブジェクトを初めて使用した後は、コピーしてはなりません。 f関数の戻り値は、once.Do(f)のすべての呼び出しの戻り値よりも「前に同期」されます。
最後のポイントは、fの実行が終了した後、その結果がonce.Do(f)を呼び出すすべてのゴルーチンに表示され、メモリの一貫性が保証されることを意味します。
💡 典型的な使用シナリオ
- Singletonパターン: データベース接続プール、構成読み込みなどが一度だけ初期化されるようにする
- 遅延ロード: 必要なときにのみリソースをロードし、一度だけロードする
- 同時実行セーフな初期化: マルチゴルーチン環境での安全な初期化
🚀 クイックスタート
sync.Onceは非常に使いやすく、Doという1つのコアメソッドしかありません。
package main import ( "fmt" "sync" ) func main() { var once sync.Once OnceBody := func() { fmt.Println("一度だけ") } // 同時に呼び出すために10個のゴルーチンを開始 done := make(chan bool) for i := 0; i < 10; i++ { go func() { Once.Do(OnceBody) done <- true }() } // すべてのゴルーチンが完了するまで待機 for i := 0; i < 10; i++ { <-done } }
実行結果は常に次のようになります。
一度だけ
単一のゴルーチンで複数回呼び出された場合でも、結果は同じです — 関数は一度だけ実行されます。
🔍 ソースコードの深い分析
sync.Onceのソースコードは非常に簡潔ですが(コメントを含めて78行のみ)、巧妙な設計が含まれています。
type Once struct { Done atomic.Uint32 // 操作が実行されたかどうかを識別 M Mutex // ミューテックスロック } func (o *Once) Do(f func ()) { IF o.Done.Load() == 0 { O.DoSlow(f) // スローパス、高速パスのインライン化を許可 } } func (o *Once) DoSlow(f func ()) { O.M.Lock() defer o.M.Unlock() IF o.Done.Load() == 0 { defer o.Done.Store(1) F() } }
設計のハイライト:
-
ダブルチェックロック:
- 最初のチェック(ロックなし):実行されたかどうかをすばやく判断
- 2回目のチェック(ロック後):同時実行の安全性を確保
-
パフォーマンスの最適化:
- ポインタオフセット計算を減らすために、doneフィールドは構造体の先頭に配置されます
- 高速パスと低速パスの分離により、高速パスのインライン化の最適化が可能になります
- ロックは最初の実行にのみ必要であり、後続の呼び出しのオーバーヘッドはゼロです
-
CASで実装しないのはなぜですか?: コメントは明確に説明しています:単純なCASでは、fが実行を終了した後にのみ結果が返されることを保証できず、他のゴルーチンが未完成の結果を取得する可能性があります。
⚠️ 注意事項
-
コピー不可: OnceにはnoCopyフィールドが含まれており、最初の使用後にコピーすると未定義の動作につながります
// 間違った例 var once sync.Once once2 := once // コンパイルはエラーを報告しませんが、実行時に問題が発生する可能性があります
-
再帰呼び出しを避ける: fでonce.Do(f)が再度呼び出された場合、デッドロックが発生します
-
パニック処理: fでパニックが発生した場合、実行済みと見なされ、後続の呼び出しではfは実行されません
✨ Go 1.21の新機能
Go 1.21では、sync.Onceファミリに3つの実用的な機能が追加され、その機能が拡張されました。
1. OnceFunc: パニック処理を備えた単一実行関数
func OnceFunc(f func()) func()
特徴:
- fを一度だけ実行する関数を返します
- fがパニックになると、返される関数は呼び出しごとに同じ値でパニックになります
- 並行処理セーフ
例:
package main import ( "fmt" "sync" ) func main() { // 一度だけ実行される関数を作成します Initialize := sync.OnceFunc(func() { fmt.Println("初期化が完了しました") }) // 並行呼び出し var wg sync.WaitGroup for i := 0; i < 5; i++ { Wg.Add(1) go func() { defer wg.Done() Initialize() }() } Wg.Wait() }
ネイティブのonce.Doと比較して:fがパニックになると、OnceFuncは呼び出しごとに同じ値を再度パニックさせますが、ネイティブのDoは最初にのみパニックになります。
2. OnceValue:単一計算および戻り値
func OnceValue [T any](f func () T) func () T
結果を計算してキャッシュする必要があるシナリオに適しています。
package main import ( "fmt" "sync" ) func main() { // 一度だけ計算する関数を作成します Calculate := sync.OnceValue(func() int { fmt.Println("複雑な計算を開始します") Sum := 0 for i := 0; i < 1000000; i++ { Sum += i } return Sum }) // 複数回の呼び出し、最初の計算のみ var wg sync.WaitGroup for i := 0; i < 5; i++ { Wg.Add(1) go func() { defer wg.Done() fmt.Println("結果:", calculate()) }() } Wg.Wait() }
3. OnceValues:2つの値を返すためのサポート
func OnceValues [T1, T2 any](f func () (T1, T2)) func () (T1, T2)
Go関数のidiomである(値、エラー)の返しに完全に適応します。
package main import ( "fmt" "os" "sync" ) func main() { // ファイルを一度だけ読み取ります ReadFile := sync.OnceValues(func() ([]byte, error) { fmt.Println("ファイルを読み取り中") return os.ReadFile("config.json") }) // 同時読み取り var wg sync.WaitGroup for i := 0; i < 3; i++ { Wg.Add(1) go func() { defer wg.Done() Data, Err := ReadFile() IF err != nil { fmt.Println("エラー:", err) return } fmt.Println("ファイル長:", len(data)) }() } Wg.Wait() }
🆚 機能比較
関数 | 特徴 | 適用可能なシナリオ |
---|---|---|
Once.Do | 基本バージョン、戻り値なし | 簡単な初期化 |
OnceFunc | パニック処理あり | エラー処理が必要な初期化 |
OnceValue | 単一の値を返すためのサポート | 結果の計算とキャッシュ |
OnceValues | 2つの値を返すためのサポート | エラーが返される操作 |
新しい関数は、エラー処理が優れており、より直感的なインターフェイスを提供するため、最初に新しい関数を使用することをお勧めします。
🎬 実用的なアプリケーションケース
1. シングルトンパターンの実装
type Database struct { // データベース接続情報 } var ( DbInstance *Database DbOnce sync.Once ) func GetDB() *Database { DbOnce.Do(func() { // データベース接続を初期化します DbInstance = &Database{ // 構成情報 } }) return DbInstance }
2. 構成の遅延ロード
type Config struct { // 構成項目 } var LoadConfig = sync.OnceValue(func() *Config { // ファイルまたは環境変数から構成をロードします Data, _ := os.ReadFile("config.yaml") var cfg Config _ = yaml.Unmarshal(data, &cfg) return &cfg }) // 使用法 func main() { Cfg := LoadConfig() // 構成を使用します... }
3. リソースプールの初期化
var InitPool = sync.OnceFunc(func() { // 接続プールを初期化します Pool = NewPool( WithMaxConnections(10), WithTimeout(30*time.Second), ) }) func GetResource() (*Resource, error) { InitPool() // プールが初期化されていることを確認します Return pool.Get() }
🚀 パフォーマンスに関する考慮事項
sync.Onceは、優れたパフォーマンスを備えています。最初の呼び出しのオーバーヘッドは、主にミューテックスロックから発生し、後続の呼び出しのオーバーヘッドはほぼゼロです。
- 最初の呼び出し:約50〜100ns(ロックの競合によって異なります)
- 後続の呼び出し:約1〜2ns(アトミックロード操作のみ)
高並行シナリオでは、(ミューテックスロックなどの)他の同期方法と比較して、パフォーマンス損失を大幅に削減できます。
📚 まとめ
sync.Onceは、同時実行環境での単一実行の問題を非常にシンプルな設計で解決し、そのコアアイデアは学ぶ価値があります。
- 最小限のオーバーヘッドでスレッドセーフを実装する
- 高速パスと低速パスを分離してパフォーマンスを最適化する
- 明確なメモリモデルの保証
Go 1.21で追加された3つの新しい関数は、その実用性をさらに向上させ、単一実行ロジックをより簡潔かつ堅牢にします。
sync.Onceファミリをマスターすると、同時初期化やシングルトンパターンなどのシナリオを簡単に処理し、よりエレガントで効率的なGoコードを作成できます。
[Leapcell:最高のサーバーレスWebホスティング](https://leapcell.io/)
最後に、Goサービスを展開するための最適なプラットフォームをお勧めします:Leapcell
🚀 お好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無制限のプロジェクトを無料でデプロイ
使用した分だけを支払う—リクエストも料金もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティのみです。
🔹 Twitterでフォローしてください:@LeapcellHQ