4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goのcontext実装にお手本のようなDouble-Checked Lockingが利用されていた話

Posted at

cancelCtx.doneの型変更

Goの標準ライブラリを読んだことがある人ならば、contextパッケージを読んだことがある人が多いのではないだろうか。

自分もその一人で、context.Contextの機能自体が好きなのもあるが、contextパッケージの実装はGoらしさが詰まっていて、十分短い(600秒未満)のでサクッと読めてとても参考になる。

過去3回ほど読み直しているが久しぶりに読み直してみたら、「おっ」と思うような修正が入っていた。

以下の一年前のパッチである。
https://github.com/golang/go/commit/ae1fa08e4138c49c8e7fa10c3eadbfca0233842b

下がdiffの一部であるが、cancelCtx.doneの型が chan struct{}からatomic.Valueに変更されているではないか。

スクリーンショット 2022-04-09 0.13.58.png

issueを追っかけると、どうやら、複数のGoルーチンが同時に、同一コンテキストに対してキャンセルされたかをcheckする状況においてパフォーマンスが悪化するのが発覚し、Done() <-chan struct{}における排他ロックをDouble-Checked Locking(後述)に修正したらしい。

今回新規に追加された以下のベンチマークだと-96.74%の改善されたようだ。
Double-Checked Lockingの威力を感じる数字だ。

func BenchmarkContextCancelDone(b *testing.B) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			select {
			case <-ctx.Done():
			default:
			}
		}
	})
}
name                 old time/op  new time/op  delta
ContextCancelDone-8  67.5ns ±10%   2.2ns ±11%  -96.74%  (p=0.000 n=30+28)

Double-Checked Lockingとは

yamasa-sanのhttps://yamasa.hatenablog.jp/entry/20100128/1264693781 が良い。

ポイントを引用すると

DCLとは、「ロック→条件判定」というロジックを「条件判定→ロック→(再度)条件判定」と書き換えるイディオムで、主に遅延初期化などの処理においてロックのオーバーヘッドを減らすために用いられます。

このDCL実装の肝は、変数 p がatomic型として宣言されている点です。 C++0x のatomic型は、これまで繰り返し説明してきたように、操作がアトミック(不可分)に行われるだけでなく、メモリアクセスの順序付けを保証する効果(メモリバリア)も持っています。変数 p への読み書きの際にメモリバリア効果が発揮されるため、最初に挙げた「DCLの問題点」を解消できているのです。

今回のソースでいうと、修正前は、「c.muにてlockして初期化済みか判定して必要であればc.doneを初期化」していた。

それが、c.doneatomic.Valueとして格納するよう修正した上で、「c.doneにて初期化済みか判定して不要ならreturn、必要ならc.muにてlockした後、再度判定して必要であればc.doneを初期化」している。

一番大事なのは、操作がアトミック(不可分)に行われるだけでなく、メモリバリア(メモリフェンスとも言う)効果がc.doneの読み書きにあるのか?ということである。

c++だと、https://en.cppreference.com/w/cpp/atomic/memory_order にあるように保証され、なおかつさまざまな保証レベルを選択できる。

Goの場合はどうかというと常にSequentially-consistentであることを保証してくれているようだ。

実は、2022年4月現在だとGoのMemory Modelのドキュメントconsistencyと言う単語が見つからず不安になるのだが、documentに書くようrscが進めてくれているようである。

また、rscが過去に

Yes, I spent a while on this last winter but didn't get a chance to write it up properly yet. The short version is that I'm fairly certain the rules will be that Go's atomics guarantee sequential consistency among the atomic variables (behave like C/C++'s seqconst atomics), and that you shouldn't mix atomic and non-atomic accesses for a given memory word.

発言している。

どうやら、C++と同義のSequentially-consistentを期待して良さそうである。

おまけ

Double-Checked Lockingはわかったよ!でも、やりたいことはchan struct{} の初期化を遅延させたいだけなのにコードが複雑すぎない?と思うかもしれない。自分もそう思う。通常業務で同じ状況に遭遇すれば明らかにメンテの複雑さによるコストを避けるために遅延させず、構造体初期化時にmake(chan struct{})するだろう。

でも、contextパッケージにおいてはそういうわけにはいかない。パフォーマンス最優先である。
実は、当初は構造体初期化時にmake(chan struct{})していたようで、遅延するようになったのは2017年の以下のcommitである。
https://github.com/golang/go/commit/986768de7fcf4def65cecd7eb0c34e2cdf92e78c

それなりに、メモリ使用量を抑えられているのがわかるだろう。

ただ、bradfitzが指摘するように当時の修正やbenchmarkではlockが新たに必要になるデメリットの方を考慮しきれていなかったようである。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?