Open1

コードリーディング - sync.Once (Golang)

ta.toshiota.toshio

こちらのイベントに参加してsync.Onceのコードリーディングをしたのでそのメモです。
https://tenntenn.connpass.com/event/239966/

現在のマスター1.17(2022-02-28)とその前のもっとシンプルだったバージョンを比較して読んだのが面白かったです。

この記事より詳しいsync.Onceコードの解説
https://zenn.dev/sryoya/articles/b0e8e8d83032b0

バージョン1.4 ~ 12.9

最初のバージョン

https://cs.opensource.google/go/go/+/refs/tags/go1.12.9:src/sync/once.go

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
	"sync/atomic"
)

// Once is an object that will perform exactly one action.
type Once struct {
	m    Mutex
	done uint32
}

// Do calls the function f if and only if Do is being called for the
// Doが関数fを呼び出すのは、Doが呼び出されたときだけです。
// first time for this instance of Once. In other words, given
// 	var once 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. 
// // once.Do(f)が複数回呼ばれた場合、最初の呼び出しだけがfを呼び出すことになります。
// A new instance of Once is required for each function to execute.
// 各関数を呼びたい場合は新しいOnceのインスタンスが必要です。
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
// Doは一度だけ実行される必要がある初期化用です。
// fはniladicであるため、引数をキャプチャーするために
// 関数リテラルを使う必要があるかもしれません。
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
// fの呼び出しが1回戻るまでDoの呼び出しは戻らないので、もしfが原因でDoが呼ばれるとデッドロックになります。
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
// f がパニックに陥った場合、Do はそれが返されたと見なし、それ以降の Do の呼び出しで返される を呼び出すことなく、fを呼び出す。
func (o *Once) Do(f func()) {
        // atomic パッケージは,同期アルゴリズムを実装する際に役立つ低レベルのアトミック基本要素を提供します。
	// LoadUint32 は *addr を自動的にロードします。
	// https://xn--go-hh0g6u.com/pkg/sync/atomic/
	// doneフラグが1だったら早期リターン
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
	// Slow-path.
	// 上のreturn(Hot-path≒Fast-path)と比べて独自関数f()は遅いpathにあたるので。
	// mutexを利用してロック
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		// f()が完了したら、doneフラグを1
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

とにかく短いコードですね。

バージョン1.17

https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/sync/once.go

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
	"sync/atomic"
)

// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
//
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/386),
	// and fewer instructions (to calculate offset) on other architectures.
	// 前のバージョンは以下の順番だったのに、doneを先にさせた。
	// 先にさせた方がdoneにアクセスするスピードがアーキテクチャ的に早くなるとのこと。
	// (参考: https://zenn.dev/sryoya/articles/b0e8e8d83032b0)
	//m    Mutex
	//done uint32
	done uint32
	m    Mutex
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// 	var once 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.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
        // // f()を実行する前にdoneを1にするのは間違いだ、と。
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// Once.Doの呼び出し完了時は必ずf()は終わっていることを確実にしてあげる。
	// 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.
	// 同時に2つのOnce.Doが呼ばれたら、実行している処理とすぐ返る処理に出くわすだろう。
	// すぐ返る方はもしかしたらf()が終わっていないのに、処理が返ってきてしまう恐れがある。
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.
	// なのでslow path(doSlow(f)(すぐ下の処理))はmutexでロックさせて
	// f()が完了するまで処理待ちさせてatomic.StoreUint32を利用して読み込みをしている。

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

func (o *Once) doSlow(f func()) {
	// 以下は前のバージョンのコメント// Slow-path.にあったものと同じ処理
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}