オペレーティングシステムにおいて、各プロセスには固有のプロセス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]
の中の1
がgoid
であると推測できます。では、プログラム内で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_tls
はruntime/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_offset
はgoid
メンバのオフセットです。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.g
とgo.itab.*runtime.g
型情報も生成されて、メソッド付きの型情報を表します。
もしg
構造体の型を表すtype·runtime·g
とg
ポインタを取得できれば、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
最後に、Goサービスをデプロイする最適なプラットフォームをお薦めします:leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に応じて支払います — リクエストがなければ、料金はかかりません。
3. 比類なきコスト効率
- 使った分だけ支払い、アイドル時の料金はありません。
- 例:25ドルで平均応答時間60msの694万件のリクエストをサポートします。
4. 簡素化された開発者体験
- 直感的なUIで簡単なセットアップが可能です。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能なインサイトを得るためのリアルタイムメトリクスとロギング。
5. 簡単なスケーラビリティと高性能
- 自動スケーリングで高い並列性を簡単に処理できます。
- オペレーションオーバーヘッドはゼロ — ビルドに集中できます。
詳細はドキュメントを参照してください。
Leapcell Twitter: https://x.com/LeapcellHQ