0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goroutine ID を取得する方法

Posted at

Group81.png

オペレーティングシステムにおいて、各プロセスには固有のプロセスIDがあり、各スレッドには独自のスレッドIDがあります。同様に、Go言語では、各ゴルーチンには独自のゴルーチンIDがあり、panicのようなシナリオでよく遭遇します。ゴルーチンには固有のIDがあるものの、Go言語は意図的にこのIDを取得するインターフェースを提供していません。今回は、Goアセンブリ言語を通じてゴルーチンIDを取得してみます。

1. goidを持たない公式設計(https://github.com/golang/go/issues/22770)

公式の関連資料によると、Go言語が意図的にgoidを提供しない理由は、乱用を避けるためです。多くのユーザーは、goidを簡単に取得した後、後続のプログラミングでgoidに強く依存するコードを無意識に書いてしまうからです。goidへの強い依存は、このコードの移植を難しくし、また並行モデルを複雑化します。同時に、Go言語には膨大な数のゴルーチンが存在する可能性がありますが、各ゴルーチンが破棄されるときにリアルタイムで監視することは容易ではなく、goidに依存するリソースが自動的に回収されないこともあります(手動での回収が必要です)。ただし、もしあなたがGoアセンブリ言語のユーザーであれば、これらの懸念を完全に無視することができます。

注意: goidを強制的に取得すると、「恥をかかされる」かもしれません 😂:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120

2. 純粋なGoでのgoid取得

理解を助けるために、まず純粋なGoでgoidを取得してみましょう。純粋なGoでgoidを取得するパフォーマンスは比較的低いですが、コードの移植性は良く、他の方法で取得したgoidが正しいかどうかをテストおよび検証するためにも使用できます。

すべてのGo言語ユーザーはpanic関数を知っているはずです。panic関数を呼び出すと、ゴルーチンが例外を起こします。もしpanicがゴルーチンのルート関数に到達する前にrecover関数によって処理されない場合、ランタイムは関連する例外とスタック情報を出力し、ゴルーチンを終了します。

panicを通じてgoidを出力する簡単な例を構築してみましょう:

package main

func main() {
    panic("leapcell")
}

実行すると、以下の情報が出力されます:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40

Panic出力情報のgoroutine 1 [running]の中の1goidであると推測できます。では、プログラム内でpanicの出力情報をどう取得することができるでしょうか?実際、上記の情報は現在の関数呼び出しスタックフレームのテキスト記述に過ぎません。runtime.Stack関数はこの情報を取得する機能を提供しています。

runtime.Stack関数に基づいて例を再構築し、現在のスタックフレームの情報を出力することでgoidを出力してみましょう:

package main

import "runtime"

func main() {
    var buf = make([]byte, 64)
    var stk = buf[:runtime.Stack(buf, false)]
    print(string(stk))
}

実行すると、以下の情報が出力されます:

goroutine 1 [running]:
main.main()
    /path/to/main.g

runtime.Stackで取得した文字列からgoid情報を解析するのは簡単です:

import (
    "fmt"
    "strconv"
    "strings"
    "runtime"
)

func GetGoid() int64 {
    var (
        buf [64]byte
        n   = runtime.Stack(buf[:], false)
        stk = strings.TrimPrefix(string(buf[:n]), "goroutine")
    )

    idField := strings.Fields(stk)[0]
    id, err := strconv.Atoi(idField)
    if err!= nil {
        panic(fmt.Errorf("can not get goroutine id: %v", err))
    }

    return int64(id)
}

GetGoid関数の詳細については説明しません。runtime.Stack関数は、現在のゴルーチンのスタック情報だけでなく、すべてのゴルーチンのスタック情報も取得できることに注意してください(2番目のパラメータで制御されます)。同時に、Go言語のnet/http2.curGoroutineID関数も同様の方法でgoidを取得しています。

3. g構造体からのgoid取得

公式のGoアセンブリ言語ドキュメントによると、各実行中のゴルーチン構造体のgポインタは、現在実行中のゴルーチンが存在するシステムスレッドのローカルストレージTLSに格納されています。まずTLSスレッドローカルストレージを取得し、その後TLSからg構造体のポインタを取得し、最後にg構造体からgoidを抽出することができます。

runtimeパッケージで定義されているget_tlsマクロを参照してgポインタを取得するコードは以下の通りです:

get_tls(CX)
MOVQ g(CX), AX     // gをAXに移動する。

get_tlsruntime/go_tls.hヘッダーファイルで定義されたマクロ関数です。

AMD64プラットフォームの場合、get_tlsマクロ関数は以下のように定義されています:

#ifdef GOARCH_amd64
#define        get_tls(r)        MOVQ TLS, r
#define        g(r)        0(r)(TLS*1)
#endif

get_tlsマクロ関数を展開すると、gポインタを取得するコードは以下の通りです:

MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX

実際、TLSはスレッドローカルストレージのアドレスに似ており、そのアドレスに対応するメモリ内のデータがgポインタです。もっと直接的には:

MOVQ (TLS), AX

上記の方法に基づいて、gポインタを取得するgetg関数をラップすることができます:

// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, $0-8
    MOVQ (TLS), AX
    MOVQ AX, ret+0(FP)
    RET

そして、Goコードでは、g構造体のgoidメンバのオフセットを通じてgoidの値を取得します:

const g_goid_offset = 152 // Go1.10

func GetGroutineId() int64 {
    g := getg()
    p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
    return *p
}

ここで、g_goid_offsetgoidメンバのオフセットです。g構造体はruntime/runtime2.goを参照してください。

Go1.10バージョンでは、goidのオフセットは152バイトです。したがって、上記のコードはgoidのオフセットも152バイトのGoバージョンでのみ正しく動作します。偉大なトンプソンの神託によれば、列挙とブルートフォースはすべての難問の万能薬です。goidのオフセットをテーブルに保存し、その後Goバージョン番号に応じてgoidのオフセットを照会することもできます。

以下は改善されたコードです:

var offsetDictMap = map[string]int64{
    "go1.10": 152,
    "go1.9":  152,
    "go1.8":  192,
}

var g_goid_offset = func() int64 {
    goversion := runtime.Version()
    for key, off := range offsetDictMap {
        if goversion == key || strings.HasPrefix(goversion, key) {
            return off
        }
    }
    panic("unsupported go version:"+goversion)
}()

これで、goidのオフセットは最終的にリリースされたGo言語バージョンに自動的に適応することができます。

4. g構造体に対応するインターフェースオブジェクトの取得

列挙とブルートフォースはシンプルですが、開発中の未リリースのGoバージョンをうまくサポートしていません。開発中の特定のバージョンでのgoidメンバのオフセットを事前に知ることはできません。

もしruntimeパッケージ内であれば、unsafe.OffsetOf(g.goid)を通じてメンバのオフセットを直接取得することができます。また、反射を通じてg構造体の型を取得し、その型を通じて特定のメンバのオフセットを照会することもできます。g構造体は内部型であるため、Goコードは外部パッケージからg構造体の型情報を取得することはできません。ただし、Goアセンブリ言語では、すべてのシンボルを見ることができるので、理論的にはg構造体の型情報も取得できます。

任意の型が定義されると、Go言語はその型に対応する型情報を生成します。たとえば、g構造体はtype·runtime·g識別子を生成してg構造体の値型情報を表し、type·*runtime·g識別子を生成してポインタ型情報を表します。もしg構造体にメソッドがあれば、go.itab.runtime.ggo.itab.*runtime.g型情報も生成されて、メソッド付きの型情報を表します。

もしg構造体の型を表すtype·runtime·ggポインタを取得できれば、gオブジェクトのインターフェースを構築することができます。以下は改善されたgetg関数で、gポインタオブジェクトのインターフェースを返します:

// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
    // get runtime.g
    MOVQ (TLS), AX
    // get runtime.g type
    MOVQ $type·runtime·g(SB), BX

    // convert (*g) to interface{}
    MOVQ AX, 8(SP)
    MOVQ BX, 0(SP)
    CALL runtime·convT2E(SB)
    MOVQ 16(SP), AX
    MOVQ 24(SP), BX

    // return interface{}
    MOVQ AX, ret+0(FP)
    MOVQ BX, ret+8(FP)
    RET

ここで、AXレジスタはgポインタに対応し、BXレジスタはg構造体の型に対応します。そして、runtime·convT2E関数は型をインターフェースに変換するために使用されます。g構造体のポインタ型を使用していないため、返されるインターフェースはg構造体の値型を表します。理論的には、gポインタ型のインターフェースも構築することができますが、Goアセンブリ言語の制限により、type·*runtime·g識別子を使用することはできません。

gが返すインターフェースに基づいて、goidを取得するのは簡単です:

import (
    "reflect"
)

func GetGoid() int64 {
    g := getg()
    gid := reflect.ValueOf(g).FieldByName("goid").Int()
    return gid
}

上記のコードは反射を通じて直接goidを取得しています。理論的には、反射されるインターフェースの名前とgoidメンバの名前が変更されない限り、コードは正常に動作するはずです。実際のテストでは、上記のコードは、Go1.8、Go1.9、Go1.10バージョンで正しく動作することが確認されています。楽観的に見ると、もしg構造体の型名が変更されず、Go言語の反射メカニズムにも変更がなければ、将来のGo言語バージョンでも動作するはずです。

反射はある程度の柔軟性を持っていますが、そのパフォーマンスはいつも批判の的になっています。改善のアイデアとしては、反射を初期化段階で一回だけ実行してgoidのオフセットを取得し、その後gポインタとオフセットを使ってgoidを取得することです。

以下はg_goid_offset変数の初期化コードです:

var g_goid_offset uintptr = func() uintptr {
    g := GetGroutine()
    if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {
        return f.Offset
    }
    panic("can not find g.goid field")
}()

正しいgoidオフセットを得た後、以前に述べた方法でgoidを取得します:

func GetGroutineId() int64 {
    g := getg()
    p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
    return *p
}

ここで、goidを取得するための実装アイデアは十分に整っていますが、アセンブリコードには依然として深刻なセキュリティリスクがあります。

getg関数はNOSPLITフラグでスタック分割を禁止する関数型として宣言されていますが、内部的にはより複雑なruntime·convT2E関数を呼び出しています。もしruntime·convT2E関数がスタックスペース不足に遭遇すると、スタック分割操作をトリガーする可能性があります。スタックが分割されると、GCは関数の引数、戻り値、ローカル変数内のスタックポインタを移動させます。ただし、getg関数はローカル変数のポインタ情報を提供していません。

以下は改善されたgetg関数の完全な実装です:

// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
    NO_LOCAL_POINTERS

    MOVQ $0, ret_type+0(FP)
    MOVQ $0, ret_data+8(FP)
    GO_RESULTS_INITIALIZED

    // get runtime.g
    MOVQ (TLS), AX

    // get runtime.g type
    MOVQ $type·runtime·g(SB), BX

    // convert (*g) to interface{}
    MOVQ AX, 8(SP)
    MOVQ BX, 0(SP)
    CALL runtime·convT2E(SB)
    MOVQ 16(SP), AX
    MOVQ 24(SP), BX

    // return interface{}
    MOVQ AX, ret_type+0(FP)
    MOVQ BX, ret_data+8(FP)
    RET

ここで、NO_LOCAL_POINTERSは関数にローカルポインタ変数がないことを意味します。同時に、返されるインターフェースはゼロ値で初期化され、初期化が完了した後、GO_RESULTS_INITIALIZEDを使ってGCに通知します。これにより、スタックが分割されたときに、GCが戻り値とローカル変数内のポインタを正しく処理できるようになります。

5. goidの応用:ローカルストレージ

goidを使えば、ゴルーチンローカルストレージを簡単に構築することができます。glsパッケージを定義してgoid機能を提供することができます:

package gls

var gls struct {
    m map[int64]map[interface{}]interface{}
    sync.Mutex
}

func init() {
    gls.m = make(map[int64]map[interface{}]interface{})
}

glsパッケージ変数は単にmapをラップし、sync.Mutexミューテックスを使って並行アクセスをサポートしています。

その後、内部のgetMap関数を定義して、各ゴルーチンのmapを取得します:

func getMap() map[interface{}]interface{} {
    gls.Lock()
    defer gls.Unlock()

    goid := GetGoid()
    if m, _ := gls.m[goid]; m!= nil {
        return m
    }

    m := make(map[interface{}]interface{})
    gls.m[goid] = m
    return m
}

ゴルーチンのプライベートなmapを取得した後、追加、削除、変更操作の通常のインターフェースです:

func Get(key interface{}) interface{} {
    return getMap()[key]
}

func Put(key interface{}, v interface{}) {
    getMap()[key] = v
}

func Delete(key interface{}) {
    delete(getMap(), key)
}

最後に、Clean関数を提供してゴルーチンに対応するmapリソースを解放します:

func Clean() {
    gls.Lock()
    defer gls.Unlock()

    delete(gls.m, GetGoid())
}

このように、最小限のゴルーチンローカルストレージglsオブジェクトが完成します。

以下はローカルストレージを使った簡単な例です:

import (
    "fmt"
    "sync"
    "gls/path/to/gls"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(idx int) {
            defer wg.Done()
            defer gls.Clean()

            defer func() {
                fmt.Printf("%d: number = %d\n", idx, gls.Get("number"))
            }()
            gls.Put("number", idx+100)
        }(i)
    }
    wg.Wait()
}

ゴルーチンローカルストレージを通じて、異なるレベルの関数がストレージリソースを共有することができます。同時に、リソースリークを避けるために、ゴルーチンのルート関数でdefer文を使ってgls.Clean()関数を呼び出し、リソースを解放する必要があります。

Leapcell: The Advanced Serverless Platform for Hosting Golang Applications

barndpic.png

最後に、Goサービスをデプロイする最適なプラットフォームをお薦めします:leapcell

1. 多言語サポート

  • JavaScript、Python、Go、またはRustで開発できます。

2. 無制限のプロジェクトを無料でデプロイ

  • 使用量に応じて支払います — リクエストがなければ、料金はかかりません。

3. 比類なきコスト効率

  • 使った分だけ支払い、アイドル時の料金はありません。
  • 例:25ドルで平均応答時間60msの694万件のリクエストをサポートします。

4. 簡素化された開発者体験

  • 直感的なUIで簡単なセットアップが可能です。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • アクション可能なインサイトを得るためのリアルタイムメトリクスとロギング。

5. 簡単なスケーラビリティと高性能

  • 自動スケーリングで高い並列性を簡単に処理できます。
  • オペレーションオーバーヘッドはゼロ — ビルドに集中できます。

詳細はドキュメントを参照してください。

Leapcell Twitter: https://x.com/LeapcellHQ

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?