ホームページ >バックエンド開発 >Golang >Goroutine ID を取得するにはどうすればよいですか?

Goroutine ID を取得するにはどうすればよいですか?

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2025-01-04 10:45:35150ブラウズ

How to Get the Goroutine ID?

オペレーティング システムでは、各プロセスには一意のプロセス ID があり、各スレッドには独自の一意のスレッド ID があります。同様に、Go 言語では、各ゴルーチンには独自の一意の Go ルーチン ID があり、パニックなどのシナリオでよく発生します。 Goroutine には固有の ID がありますが、Go 言語はこの ID を取得するためのインターフェイスを意図的に提供していません。今回は、Go アセンブリ言語を使用して Goroutine 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. Pure Go で goid を取得する

理解を容易にするために、最初に純粋な Go で goid を取得してみます。純粋な Go で goid を取得するパフォーマンスは比較的低いですが、コードの移植性が高く、他のメソッドで取得した goid が正しいかどうかのテストや検証にも使用できます。

すべての Go 言語ユーザーはパニック関数を知っている必要があります。パニック関数を呼び出すと、Goroutine 例外が発生します。 Goroutine のルート関数に到達する前にリカバリ関数によってパニックが処理されなかった場合、ランタイムは関連する例外とスタック情報を出力し、Goroutine を終了します。

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

package main

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

実行後、次の情報が出力されます:

panic: leapcell

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

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

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

package main

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

実行後、次の情報が出力されます:

panic: leapcell

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

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

package main

import "runtime"

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

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

3. g 構造体から goid を取得する

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

次は、ランタイム パッケージで定義されている get_tls マクロを参照して g ポインターを取得します。

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

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

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

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)
}

get_tls マクロ関数を展開した後、g ポインターを取得するコードは次のとおりです。

get_tls(CX)
MOVQ g(CX), AX     // Move g into AX.

実際、TLS はスレッドのローカルストレージのアドレスに似ており、そのアドレスに対応するメモリ上のデータが g ポインタです。もっと単純に言うこともできます:

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

上記のメソッドに基づいて、getg 関数をラップして g ポインターを取得できます。

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

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

MOVQ (TLS), AX

ここで、g_goid_offset は goid メンバーのオフセットです。 g 構造体は runtime/runtime2.go.

を参照します。

Go1.10バージョンでは、goidのオフセットは152バイトです。したがって、上記のコードは、goid オフセットも 152 バイトである Go バージョンでのみ正しく実行できます。偉大なトンプソンの神託によると、列挙と力技はすべての困難な問題に対する万能薬です。 goid オフセットをテーブルに保存し、Go バージョン番号に基づいて goid オフセットをクエリすることもできます。

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

// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10

func GetGroutineId() int64 {
    g := getg()
    p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
    return *p
}
-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET

これで、ついに goid オフセットがリリースされた Go 言語バージョンに自動的に適応できるようになりました。

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

列挙と総当たりは簡単ですが、開発中の未リリースの Go バージョンを十分にサポートしていません。開発中の特定のバージョンにおける goid メンバーのオフセットを事前に知ることはできません。

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

型が定義されると、Go 言語はその型に対応する型情報を生成します。たとえば、g 構造体は、g 構造体の値型情報を表す type-runtime-g 識別子と、ポインタ型情報を表す type-*runtime-g 識別子を生成します。 g 構造体にメソッドがある場合、メソッドで型情報を表す go.itab.runtime.g および go.itab.*runtime.g の型情報も生成されます。

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

package main

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

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

g によって返されたインターフェースに基づいて、goid を簡単に取得できます。

panic: leapcell

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

上記のコードは、リフレクションを通じて goid を直接取得します。理論的には、反映されたインターフェイスと goid メンバーの名前が変更されない限り、コードは正常に実行できます。実際のテストの後、上記のコードは Go1.8、Go1.9、および Go1.10 バージョンで正しく実行できます。楽観的に考えれば、g 構造型の名前が変更されず、Go 言語のリフレクション メカニズムが変更されなければ、将来の Go 言語バージョンでも実行できるはずです。

リフレクションにはある程度の柔軟性がありますが、リフレクションのパフォーマンスは常に批判されてきました。改善されたアイデアは、リフレクションを通じて goid のオフセットを取得し、次に g ポインターとオフセットを通じて goid を取得することです。これにより、リフレクションは初期化フェーズで 1 回だけ実行する必要があります。

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

package main

import "runtime"

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

正しい goid オフセットを取得した後、前述の方法で goid を取得します。

package main

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

この時点で、goid を取得するための実装アイデアは十分に完成しましたが、アセンブリ コードには依然として重大なセキュリティ リスクが存在します。

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

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

panic: leapcell

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

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

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

goid を使用すると、Goroutine のローカル ストレージを非常に簡単に構築できます。 goid 機能を提供する gls パッケージを定義できます。

package main

import "runtime"

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

gls パッケージ変数は単純にマップをラップし、sync.Mutex ミューテックスを介した同時アクセスをサポートします。

次に、内部 getMap 関数を定義して、Goroutine バイトごとにマップを取得します。

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

Goroutine のプライベート マップを取得した後は、追加、削除、および変更操作のための通常のインターフェイスになります。

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)
}

最後に、Goroutine に対応するマップ リソースを解放する Clean 関数を提供します。

get_tls(CX)
MOVQ g(CX), AX     // Move g into AX.

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

次に、ローカル ストレージを使用する簡単な例を示します。

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

Goroutine ローカル ストレージを介して、さまざまなレベルの関数がストレージ リソースを共有できます。同時に、リソース リークを避けるために、Goroutine のルート関数で gls.Clean() 関数を defer ステートメント経由で呼び出してリソースを解放する必要があります。

Leapcell: Golang アプリケーションをホストするための高度なサーバーレス プラットフォーム

How to Get the Goroutine ID?

最後に、Go サービスのデプロイに最も適したプラットフォームをお勧めします。leapcell

1. 多言語サポート

  • JavaScript、Python、Go、または Rust を使用して開発します。

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

  • 使用料金のみお支払いください。リクエストや料金はかかりません。

3. 比類のないコスト効率

  • アイドル料金なしの従量課金制。
  • 例: $25 は、平均応答時間 60 ミリ秒で 694 万のリクエストをサポートします。

4. 合理化された開発者エクスペリエンス

  • 直感的な UI でセットアップが簡単。
  • 完全に自動化された CI/CD パイプラインと GitOps の統合。
  • 実用的な洞察を得るリアルタイムのメトリクスとログ。

5. 容易な拡張性と高性能

  • 自動スケーリングにより、高い同時実行性を簡単に処理できます。
  • 運用上のオーバーヘッドがゼロ - 構築だけに集中できます。

ドキュメントでさらに詳しく見てみましょう!

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

以上がGoroutine ID を取得するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。