ネットでGo言語の sync.Once
についてつらつらと調べるていると
1度だけ実行する処理を書くことができる
2度目の呼び出しは実行されない
という紹介が多くあり、これは間違ってはいないし大変便利な機能であることがわかる。
ちなみにパッケージのドキュメントによれば、
Do calls the function f if and only if Do is being called for the first time for this instance of Once.
if once.Do(f) is called multiple times, only the first call will invoke f, even if f has a different value in each invocation. A new instance of Once is required for each function to execute.
via package sync
ということで、要するに
1
2
3
4
5
6
7
8
9
10
func main () {
once := sync . Once {}
for i := 0 ; i < 5 ; i ++ {
once . Do ( func (){
fmt . Printf ( "Initialize by %v\n" , i )
})
fmt . Printf ( "done %v\n" , i )
}
}
の実行結果は
Initialize by 0
done 0
done 1
done 2
done 3
done 4
となる。
sync.Once.Do()
で呼び出す中身がなんだろうと1つの sync.Once
インスタンスでは1回だけ実行される。
だがしかし、goroutine
をつかって、sync.Once.Do()
を同時に実行するとどうなるのか。
このことについて解説している文献があまりなかったので書いておく。
わかりやすいように once.Do()
の完了に3秒以上かかるようにしてみる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main () {
once := sync . Once {}
wg := sync . WaitGroup {}
for i := 0 ; i < 5 ; i ++ {
wg . Add ( 1 )
go func ( n int ) {
defer wg . Done ()
once . Do ( func (){
fmt . Printf ( "Initialize by %v\n" , n )
time . Sleep ( time . Second * 3 )
})
fmt . Printf ( "done %v\n" , n )
}( i )
}
wg . Wait ()
fmt . Println ( "all done" )
}
の実行結果は
Initialize by 4
(3秒後)
done 4
done 0
done 2
done 1
done 3
all done
のようになる。
つまり、 sync.Once.Do()
は同時に実行すると最初の呼び出しが終わるまでブロックされる。
もちろんブロックするだけで2回目以降の処理は実行されない。
パッケージのドキュメントによれば、
Because no call to Do returns until the one call to f returns, if f causes Do to be called, it will deadlock.
via package sync
のように書いてあり、これは大変重要なことだと思う。
まとめ
sync.Once
は
スレッドセーフで、
ただ1回限りの実行を保証し、
その1回の実行完了を待つ
ということができる機能。
おかげさまで、「時間のかかる初期化処理を待つ 」というようなコードは改めて書く必要がなかった、という話。
(もちろんプログラムの初期処理においては init()
で事足りる場合が多いが)
参考: https://golang.org/src/sync/once.go