Goroutine Local Storage

6711 ワード

背景
最近、呼び出しチェーンとログ追跡を設計するAPIでは、JavaとC++に比べて、Go言語にはオリジナルのスレッド(スレッド)コンテキストもTLS(Thread Local Storage)もサポートされておらず、APIがGoroutineのId(後にGoIdと略称する)を取得することも暴露されていないことが分かった.これにより、JavaのようにTLS上にいくつかの情報を配置することができず、上位アプリケーションのAPIの使用を簡略化するために使用される:呼び出しスタックの関数でパラメータを伝達することによって呼び出しチェーンとログ追跡のいくつかのコンテキスト情報を伝達する必要はない.
JavaとC++では、TLSはスレッド環境に格納された構造であり、そのスレッド内で独自に享受されるデータを格納するためのメカニズムである.プロセス内のスレッドは、自分のTLSではないスレッドにアクセスできません.これにより、TLS内のデータはスレッド内でグローバルに共有され、スレッド外では表示されません.
Javaでは、JDKライブラリは現在のスレッドオブジェクトを取得するためにThread.CurrentThread()を提供し、スレッドローカル変数を格納および取得するためにThreadLocalを提供します.JavaはThread.CurrentThread()を介して現在のスレッドを取得できるため、その実現の構想は簡単であり、ThreadLocalクラスには各スレッドの変数を格納するためのMapがある.
ThreadLocalのAPIでは、以下の4つの方法が提供されています.
public T get()
protected  T initialValue()
public void remove()
public void set(T value)
  • T get():このスレッドのローカル変数の現在のスレッドコピーの値を返します.このメソッドがスレッドによって初めて呼び出された場合、このコピーを作成して初期化します.
  • protected T initialValue():このスレッドのローカル変数の現在のスレッドの初期値を返します.このメソッドは、スレッドにアクセスして各スレッドのローカル変数を取得するたびに呼び出されます.すなわち、スレッドがget()メソッドを使用して初めて変数にアクセスしたときです.スレッドがgetメソッドより先にset(T)メソッドを呼び出す場合、initialValueメソッドはスレッドで呼び出されません.
  • void remove():このスレッドのローカル変数の値を削除します.これにより、スレッドのローカル変数のストレージ要件を低減できます.このスレッドのローカル変数に再アクセスすると、デフォルトではinitialValueがあります.
  • void set(T value)このスレッドのローカル変数の現在のスレッドコピーの値を指定値に設定します.多くのアプリケーションはこの機能を必要とせず、initialValue()メソッドに依存してスレッドのローカル変数の値を設定します.

  • Go言語では、Googleが提供する解決策はgolang.org/x/net/contextパッケージを使用してGoRoutineのコンテキストを伝達することです.GoのContextに対する深い理解は私の前の分析を参考にすることができます:Go Contextメカニズムを理解します.ContextはGoroutineの一部のデータを共有するために格納することもできますが、新しいContextオブジェクトを作成するためにWithValue関数を提供するインタフェースです.
    func WithValue(parent Context, key interface{}, val interface{}) Context {
    	return &valueCtx{parent, key, val}
    }
    
    type valueCtx struct {
    	Context
    	key, val interface{}
    }
    
    func (c *valueCtx) Value(key interface{}) interface{} {
    	if c.key == key {
    		return c.val
    	}
    	return c.Context.Value(key)
    }
    

    上のコードから分かるように、Contextが一度Valueを設定すると、Contextオブジェクトが生成されます.Valueを取得するには、現在のContextに格納されている値を探してから、親レベルに検索しないと取得されます.Valueを取得することは、インタフェースの設計上、1つのGoroutineが一度にKey/Valueを設定するだけで、他の複数のGoroutineはKeyのValueしか読めないため、マルチGoroutineアクセスセキュリティと言える.
    なぜGoIdインタフェースを取得できなかったのか
    This, among other reasons, to prevent programmers for simulating thread local storage using the goroutine id as a key.
    公式には、Goroutine IdをThread Local StorageのKeyとして採用することを避けるためだ.
    Please don’t use goroutine local storage. It’s highly discouraged. In fact, IIRC, we used to expose Goid, but it is hidden since we don’t want people to do this.
    ユーザはGoIdを使用してgoroutine local storageを実装することが多いが、Go言語はgoroutine local storageを使用することを望んでいない.
    when goroutine goes away, its goroutine local storage won’t be GCed. (you can get goid for the current goroutine, but you can’t get a list of all running goroutines)
    goroutine local storageの使用を推奨しない理由は、GCが容易ではないため、現在のGoIdは取得できますが、他の実行中のGoroutineは取得できません.
    what if handler spawns goroutine itself? the new goroutine suddenly loses access to your goroutine local storage. You can guarantee that your own code won’t spawn other goroutines, but in general you can’t make sure the standard library or any 3rd party code won’t do that.
    もう1つの重要な理由は、Goroutineの生成が非常に容易であるため(スレッドは一般的にスレッドプールを採用する)、新しく生成されたGoroutineはgoroutine local storageへのアクセスを失うことになります.新しいGoroutineが生成されないことを保証する上位アプリケーションが必要ですが、標準ライブラリまたは第3ライブラリがそうしないことを確認するのは難しいです.
    thread local storage is invented to help reuse bad/legacy code that assumes global state, Go doesn’t have legacy code like that, and you really should design your code so that state is passed explicitly and not as global (e.g. resort to goroutine local storage)
    TLSの応用は,既存の悪い(残された)グローバル状態を採用するコードの再利用を支援するものである.一方、Go言語では、グローバル状態ではなく表示的に状態を伝達するコードの再設計が推奨されています(例えばgoroutine local storage).
    その他の手段でGoIdを取得
    Go言語は意識的にGoIdを隠すが、現在はGoIdを取得する手段がある.
  • ソースコードを修正するとGoIdが露出するが、Go言語はいつでもソースコードを修正する可能性があり、
  • と互換性がない.
    標準ライブラリのruntime/proc.go(Go 1.6.3)のnewextram関数で、GoIdが生成されます.
    mp.lockedg = gp
    gp.lockedm = mp
    gp.goid = int64(atomic.Xadd64(&sched.goidgen, 1))
    
  • runtime.StackによりStack出力情報を解析してGoIdを取得する.

  • 標準ライブラリのruntime/mprof.go(Go 1.6.3)では、runtime.Stackはgpオブジェクト(GoIdを含む)を取得し、Stack情報全体を出力します.
    func Stack(buf []byte, all bool) int {
        if all {
            stopTheWorld("stack trace")
        }
    
        n := 0
        if len(buf) > 0 {
            gp := getg()
            sp := getcallersp(unsafe.Pointer(&buf))
            pc := getcallerpc(unsafe.Pointer(&buf))
            systemstack(func() {
                g0 := getg()
                g0.m.traceback = 1
                g0.writebuf = buf[0:0:len(buf)]
                goroutineheader(gp)
                traceback(pc, sp, 0, gp)
                if all {
                    tracebackothers(gp)
                }
                g0.m.traceback = 0
                n = len(g0.writebuf)
                g0.writebuf = nil
            })
        }
    
        if all {
            startTheWorld()
        }
        return n
    }
    

    ファイル名から分かるようにruntime/mprof.goはProfile分析に使われており、Stackを取得するのはパフォーマンスがあまりよくないに違いない.上のコードから見ると、2番目のパラメータがtrueに指定されている場合、STWもあり、ビジネスシステムはどうしても受け入れられません.Go言語がStackの出力を変更すると、Stack情報を解析してもGoIdが正常に取得できなくなります.
  • 汎用runtime.Callers呼び出しStackにラベル
  • を付ける
    コード参照:https://github.com/jtolds/gls/blob/master/stack_tags_main.go#L43
  • は、インラインcまたはインラインアセンブリ
  • を通過する.
    goバージョン1.5,x 86_64 arcの下でアセンブリして、推定しても通用しません
    // func GoID() int64
    TEXT s3lib GoID(SB),NOSPLIT,$0-8
    MOVQ TLS, CX
    MOVQ 0(CX)(TLS*1), AX
    MOVQ AX, ret+0(FP)
    RET
    

    オープンソースgoroutine local storage実装
    GoIdを取得するメカニズムさえあれば、Javaのようにグローバルなmapでgoroutine local storageを実現することができ、Githubで検索すると、2つあります.
  • tylerb/gls GoIdはruntime.StackによりStack出力情報を解析してGoIdを取得する.
  • jtolds/gls GoIdは、呼び出しStackにラベル
  • を付けるために汎用runtime.Callersである.
    2番目に2013年に性能をテストした人がいます.データは以下の通りです.
    BenchmarkGetValue 500000 2953 ns/op BenchmarkSetValues 500000 4050 ns/op
    上記のテスト結果は悪くないようですが、goroutine local storageの実現はmap+RWMutexにほかならず、パフォーマンスのボトルネックがあります.
  • GoroutineはThreadとは違って、その個数は10万以上同時に合併することができ、こんなに多くのGoroutineが同じロックを同時に競争すると、性能は急激に悪化します.
  • GoIdは、Stackを呼び出す情報を分析することによって取得され、高コストの呼び出しでもあり、1文字:遅い.

  • いずれにしても、公式のGLSがないと、確かに便利ではありません.サードパーティの実現には性能と互換性のないリスクがあります.jtolds/glsの著者も他の人の評価を貼った.
    "Wow, that’s horrifying.”
    “This is the most terrible thing I have seen in a very long time.”
    “Where is it getting a context from? Is this serializing all the requests? What the heck is the client being bound to? What are these tags? Why does he need callers? Oh god no. No no no.”
    小結
    Go言語公式では、TLSがグローバル状態を格納するのはよくない設計ではなく、表示して状態を伝達すると考えられています.Googleが与える解決策はgolang.org/x/net/contextです.