golangの encoding/json/encode.go を読んでたしかにcacheにlockは必要だな〜と思ったりした

golang の encoding/json/encode.go を読んでたしかにcacheにlockは必要だな〜と思ったりした

encoding/json付近を眺めていた。

ぱっと見reflectの使い方などの把握にも良さそうだと思ったがまだ正確には理解しきれていないのでそれに関しては触れない。

当たり前のことだけれど。golang複数のコアを使った演算がやりやすくなっているしやる機会が多い。LLなどを使っていた時にはあまり気にしなかったキャッシュの取り扱いでもそう言えばまじめにlockかけないとダメだよな〜みたいなことを思った。

lockのコード

encoding/json/encode.go でcacheを使っている場所は以下の2つ

  • encoderのcache (encoderCache)
  • fieldのcache(fieldCache)

今回はencoderの方のcacheについてしか触れない。

Lockを使ったcacheの行い方

cacheの行い方は簡単に書くと以下のようなもの

  1. sync.RWMutex をembedしたstructを作成(もしくはそれを直接使う)
  2. 実際に使う (Lock/UnLock, RLock/UnRLock)

structの作成

var cache struct {
    sync.RWMutex
    // 何か他のfield定義追加
}

実際に使う方法

cache.RLock() // もしくはcache.Lock()
doSomething()
cache.RUnlock() // もしくはcache.UnLock()

当たり前だけれど、要所要所で細かくlockを取る必要がある。

実際のencoder cacheのコード

例えばencoderのcacheは以下の様な感じで使われる。

type encoderFunc func(e *encodeState, v reflect.Value, quoted bool)

var encoderCache struct {
    sync.RWMutex
    m map[reflect.Type]encoderFunc
}

func typeEncoder(t reflect.Type) encoderFunc {
    encoderCache.RLock() // (1)
    f := encoderCache.m[t]
    encoderCache.RUnlock()
    if f != nil {
        return f
    }

    // To deal with recursive types, populate the map with an
    // indirect func before we build it. This type waits on the
    // real func (f) to be ready and then calls it.  This indirect
    // func is only used for recursive types.
    encoderCache.Lock() // (2)
    if encoderCache.m == nil {
        encoderCache.m = make(map[reflect.Type]encoderFunc)
    }
    var wg sync.WaitGroup
    wg.Add(1)
    encoderCache.m[t] = func(e *encodeState, v reflect.Value, quoted bool) {
        wg.Wait()
        f(e, v, quoted)
    }
    encoderCache.Unlock()

    // Compute fields without lock.
    // Might duplicate effort but won't hold other computations back.
    f = newTypeEncoder(t, true)
    wg.Done()
    encoderCache.Lock() // (3)
    encoderCache.m[t] = f
    encoderCache.Unlock()
    return f
}

どのような処理でlockが掛けられているかまとめる。細かく見ていく気はしないので雑に。

  1. cacheへのアクセス(get)
  2. 型からserializeのための関数を取得する部分のlock(後にwaitGroupについても説明)
  3. cacheへのアクセス(set)

1 のみつかっているのはreader用のlock(RLock)。ミスキャッシュ(キャッシュされていないmapの要素にアクセス)が許されるならreader用のlockが不要な気がするけれど。今回の用途ではミスキャッシュは許され無さそうなので、2,3の作業中のlockが掛かったら待って欲しいのでgetにも必要ということだと思う。

2 は初めmapがzero値(nil?)なのでlockが必要ということだと思う。

3 は以下略。

(TODO: lockなどの実験をする)

sync.RWMutexのLockとRLockの違い

RLockとLockの違いは sync.RWMutex に説明がある。

  • RLock -- read 用のlock
  • Lock -- write 用のlock

(memo: この説明はひどい)

RLockについては同一goroutine内での複数のRLockは省略されたりするらしい。もう少し詳しく調べないとダメ。とりあえず通常のMutexでLockをかけるよりは効率は良さそう。

sync.WaitGroupについて

  • Add() -- 内部的なカウンターを増やす
  • Done() -- 内部的なカウンターを減らす
  • Wait() -- 内部的なカウンターが0になるまで待つ

今回の encoding/json/encode.go では f に束縛される関数が見つかるまで待つという意味のよう。

   var wg sync.WaitGroup
    wg.Add(1)
    encoderCache.m[t] = func(e *encodeState, v reflect.Value, quoted bool) {
        wg.Wait()
        f(e, v, quoted) // この時点で f == nilだったりすると辛い
    }
    encoderCache.Unlock()

    // Compute fields without lock.
    // Might duplicate effort but won't hold other computations back.
    f = newTypeEncoder(t, true)
    wg.Done()

よくあるsampleではgoroutineの同期を見ることが多いっぽい。

var wg sync.WaitGroup
for i, x := range xs {
    wg.add(1)
    go func(x) {
        defer wg.Done()
        doSomething(x)
    }(x)
}
wg.Wait()

参考