1. ホーム
  2. スクリプト・コラム
  3. ゴラン

Go並行プログラミング - sync.Onceの例

2022-02-13 06:17:21

I. シーケンス

名前だけで、このライブラリが何をするものか想像がつくでしょう。 sync.Once 使い方は簡単で、以下が簡単な使用例です。

package main
 
import (
	"fmt"
	"sync"
)
 
func main() {
	var (
		once sync.Once
		wg sync.WaitGroup
	)
 
	for i := 0; i < 10; i++ {
		wg.Add(1)
		// Note here that i is shown as an argument passed into the internal anonymous function
		go func(i int) {
			defer wg.Done()
			// fmt.Println("once", i)
			once.Do(func() {
				fmt.Println("once", i)
			})
		}(i)
	}
 
	wg.Wait()
	fmt.Printf("over")
}

出力します。

❯ go run . /demo.go
一度9

を追加しない場合のテスト once.Do このコードを実行すると、以下のような結果が出力され、実行ごとに異なる出力が得られます。

一回 9
1回 0
1回 3
1回 6
1回 4
1回
5回
1回 2
7回
1回 8

2回の出力の差から、次のことがわかります。 sync.Once が行うのは、入力された function は一度だけ実行されます。

II. ソースコード解析

2.1 構造体

Onceの構造は以下の通りです。

type once struct {
    done uint32
    m Mutex
m Mutex}

各 sync.Once 構造体には、ブロックが実行されたかどうかを識別するための done と、ミューテックスロック sync.Mutex のみが含まれています。

2.2 インターフェース

sync.Once.Do sync.Once 構造体が外部に公開しているメソッドのうち、空のエントリを持つ関数を受け取る唯一のメソッドである

  • 渡された関数がすでに実行されている場合は、単に
  • 入力された関数がまだ実行されていない場合は sync.Once.doSlow 入力された引数を実行する
func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	// if atomic.
	CompareAndSwapUint32(&o.done, 0, 1) { // if atomic.
	// 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)
	}
}

コードコメントで具体的に注釈をつける:間違えやすい実装

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

この実装の最大の問題は、同時呼び出しがあった場合、一方のゴルーチンが実行され、もう一方は成功した後に実行中のゴルーチンが戻るのを待たず、直接戻ってしまうことで、渡されたメソッドが先に実行される保証がないことである

正しい方法

if atomic.LoadUint32(&o.done) == 0 {
    // Outlined slow-path to allow inlining of the fast-path.
    o.doSlow(f)
}

は、まず done が 0 かどうかを判断し、0 でない場合はまだ実行されていないので

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

doSlow

done の1回のみの実行を保証するためにミューテックスロックが使用されている場合。

特定のロジック

  • 現在のGoroutineのmutexロックを取得する
  • 受信した無入力関数を実行する。
  • 遅延された関数を実行し、メンバ変数 type singleton struct {} var ( instance *singleton initialized uint32 mu sync. ) func Instance() *singleton { if atomic.LoadUint32(&initialized) == 1 { return instance } mu.Lock() defer mu.Unlock() if instance == nil { defer atomic.StoreUint32(&initialized, 1) instance = &singleton{} } return instance } を1

III. ユースケースシナリオ

3.1 シングルトンパターン

相互排他的ロックによる原子操作は、非常に効率的なシングルピース・パターンを可能にします。相互排他ロックは、通常の整数のアトミックリードおよびライトよりもはるかに高価です。パフォーマンスが重視される部分には数値のフラグビットを追加し、フラグビットの状態をアトミックに検出することで相互排他ロックが使用される回数を減らし、パフォーマンスを向上させることができる。

sync.Once

そして type singleton struct {} var ( instance *singleton once sync. ) func Instance() *singleton { once.Do(func() { instance = &singleton{} }) return instance } は、シングルトンパターンをよりシンプルに実装したものです。

var icons map[string]image.
 
func loadIcons() {
    icons = map[string]image.Image{
        "left": loadIcon("left.png"),
        "up": loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down": loadIcon("down.png"),
    }
}
 
// Icon is not concurrency-safe when called by multiple goroutines
// because the map type is not a type-safe data structure in the first place
func Icon(name string) image.Image {
    if icons == nil {
        loadIcons()
    }
    return icons[name]
}

3.2 設定ファイルの読み込みの例

オーバーヘッドの初期化操作は、実際に使用されるまで延期するのがよい方法です。なぜなら、(init関数などで)あらかじめ変数を初期化しておくとプログラムの起動時間が長くなり、実際の実行時にはその変数が使われない可能性があるため、初期化操作は必要ないからです。例を見てみましょう。

sync.Once

Icon関数を同時に呼び出す場合、複数のゴルーチンは同時並行的に安全ではなく、各ゴルーチンが直列一貫性を満足しながら、コンパイラとCPUが自由にメモリへのアクセスを並べ替えることができる。 loadIcons関数は、結果として並べ替えが可能である。

<ブロッククオート

func loadIcons() {
    icons = make(map[文字列]画像.)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")です。
    icons["down"] = loadIcon("down.png")
}

この場合、アイコンがnilでないと判断しても、変数の初期化が完了したわけではありません。このような状況を考えると、アイコンの初期化を他のゴルーチンから操作されないようにするために、mutexロックを追加する方法しか思いつきませんが、これはパフォーマンスの問題が生じます。

を使用することができます。 var icons map[string]image. var loadIconsOnce sync. func loadIcons() { icons = map[string]image.Image{ "left": loadIcon("left.png"), "up": loadIcon("up.png"), "right": loadIcon("right.png"), "down": loadIcon("down.png"), } } // Icon is concurrency-safe, and ensures that the configuration is loaded only when the code is run func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] } 変換コード

sync.Once

この設計により、初期化操作は同時並行的に安全に実行され、複数回実行されることはありません。

IV. 要約

関数の実行回数の保証として sync/atomic 構造体の場合、それはミューテックス・ロックを使用し sync.Once.Do パッケージは、関数がプログラム実行中に一度だけ実行できるというセマンティクスを実装するためのメソッドを提供します。また、この構造体を使用する場合、以下の点に注意する必要があります。

  • sync.Once.Do メソッドに渡された関数は、関数内でパニックが発生しても一度だけ実行されます。
  • を2回呼び出すと sync.Once.Do メソッドが別の関数に渡された場合、渡された関数の最初の呼び出しだけが実行されます。

V. リファレンス

この記事は、Goの並行プログラミング - sync.Onceについて書かれたものです。Goの並行プログラミングについては、Scripting Houseの過去記事を検索するか、以下の記事を引き続き閲覧してください。