
sync.Once は、あるタスクが厳密に一度だけ実行されることを保証するために使用されます。その実装は非常にシンプルです:
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()
}
}
Once の動作プロセスが見て取れます:
- atomic を使用して done が実行されたかどうかをチェックします
- Mutex を使用して、同時に1つのゴルーチンのみが実行に入ることを保証します。
- done が書き換えられたかどうかをチェックする
- 実行して done 値を更新する
全体のプロセスは実際にはダブルチェックロックシングルトンの実装であり、ロックの基礎にダブルチェックを導入しているため、以下のいくつかの点に注意する価値があります:
- なぜ複数回のチェックを設定するのでしょうか?
doSlow を観察してみましょう。これは完全で独立した作業フローですが、問題はその遅さです。毎回のチェックにはロックの競合が必要です。そのため、パフォーマンスを向上させるために、処理が完了しているかどうかを迅速に判断するために、atomic による前置きチェックを使用します。
- 前のチェックで done の値を確認したにも関わらず、ロックを取得して実行する前に再度チェックする必要があるのはなぜでしょうか?
全体のプロセスがアトミック操作ではないため、ミューテックスの待機中に、先行するゴルーチンが既に done の値を変更している可能性があります。その後に進入を許可されたゴルーチンは、done が変更されていないことを保証する必要があります。