Open8
楽観的手法を試す
参考1: On optimistic methods for concurrency control
参考2: Optimistic Lock Coupling: A Scalable and Efficient General-Purpose Synchronization Method
比較対象
- nosync
- 整合性は気にせず同期をしない
- pessimistic
- RWLock で保護
- optimistic
- read はバージョン検証、write は lock で競合させる
- Optimistic Lock Coupling 論文の実装を参考にした
- int64 の 1bit目を lock フラグ、2bit目以降をバージョン番号として利用
- read はバージョン検証、write は lock で競合させる
nosync
package main
// NoSyncTuple は同期なしでtupleを操作する実装
type NoSyncTuple struct {
tuple *Tuple
}
func NewNoSyncTuple(data string) *NoSyncTuple {
return &NoSyncTuple{
tuple: NewTuple(data),
}
}
func (nt *NoSyncTuple) Read() (string, error) {
// 同期なしで直接データを読み取り
return nt.tuple.data, nil
}
func (nt *NoSyncTuple) Write(newData string) error {
// 同期なしで直接データを書き換え
nt.tuple.data = newData
return nil
}
pessimistic
package main
import (
"sync"
)
// PessimisticTuple は悲観的手法(RWLock)で保護されたtuple
type PessimisticTuple struct {
tuple *Tuple
rwMu sync.RWMutex
}
func NewPessimisticTuple(data string) *PessimisticTuple {
return &PessimisticTuple{
tuple: NewTuple(data),
}
}
func (pt *PessimisticTuple) Read() (string, error) {
pt.rwMu.RLock()
defer pt.rwMu.RUnlock()
// データを読み取り
return pt.tuple.data, nil
}
func (pt *PessimisticTuple) Write(newData string) error {
pt.rwMu.Lock()
defer pt.rwMu.Unlock()
// データを更新
pt.tuple.data = newData
return nil
}
optimistic
package main
import (
"sync/atomic"
)
const (
LockBit = int64(1) << 0 // 1bit目 (最下位ビット)
)
// OptimisticTuple は楽観的手法で保護されたtuple
type OptimisticTuple struct {
tuple *Tuple
version int64
}
func NewOptimisticTuple(data string) *OptimisticTuple {
return &OptimisticTuple{
tuple: NewTuple(data),
version: 0,
}
}
func (ot *OptimisticTuple) Read() (string, error) {
for {
// バージョンを読み取り
ver := atomic.LoadInt64(&ot.version)
// lockされている場合は再試行
if ver&LockBit != 0 {
continue
}
// データをコピー
data := ot.tuple.data
// バージョンが変更されていないことを確認
newVer := atomic.LoadInt64(&ot.version)
if newVer == ver {
return data, nil
}
}
}
func (ot *OptimisticTuple) Write(newData string) error {
for {
// 現在のバージョンを読み取り
oldVer := atomic.LoadInt64(&ot.version)
// lockされている場合は再試行
if oldVer&LockBit != 0 {
continue
}
// ロックを取得する
if !atomic.CompareAndSwapInt64(&ot.version, oldVer, oldVer|LockBit) {
continue
}
// ロックを取得できたのでデータを更新
ot.tuple.data = newData
// バージョン番号を増加させてロックを解除
// LockBitをアトミックに加算することでバージョンを+1しつつロックフラグを落とす
atomic.AddInt64(&ot.version, LockBit)
return nil
}
}
評価
- Apple M1 Max(高性能8コア+ 高効率2コア合計10コア)で検証
- できるだけアプリは落とした状態
- ワークロードは読み取りのみ
- goroutine を 1 〜 10 と変化させて計測
結果

- nosync と optimistic
- 4 まではきれいにスケールする
- 6 でサチる
- 9 で急激に劣化
- pessimistic
- 1 が最高でそこから徐々に落ちていく
考察
- nosync と optimistic がほぼ同等
- 怪しさも感じつつ、Optimistic Lock Coupling の論文の Fig 3. もそんな感じの結果が出ているのでそんなもの?
- 6 でサチるのは?
- なんやかんやアプリは動いているので仕事に専念できてない可能性
- そもそも main の goroutine も計測結果の取りまとめなどをしている
- 9 での劣化は高効率コアの利用に起因しているかも
- 怪しさも感じつつ、Optimistic Lock Coupling の論文の Fig 3. もそんな感じの結果が出ているのでそんなもの?
- pessimistic
-
キャッシュラインを汚してしまっているためスケールしないと思われる
- 予想通りではあるが、goroutine 1 が最大スループットなのは意外
-
キャッシュミスなどを見た方が確実だが、mac であるためちょっと見るのが面倒で割愛
-

