在 Go 中如果獲取 goroutine 的 id?
如果你使用過如 Python、Java 等主流支持併發的編程語言,那麼通常都能夠比較容易的獲得進程和線程的 id。但是在 Go 語言,沒有直接提供對多進程和多線程的支持,而是提供了 goroutine 來支持併發編程。不過在 Go 中,獲取 goroutine 的 id 並不像其他編程語言那樣容易,但依然有辦法,本文就來介紹下如何實現。
獲取當前進程的 id
首先,雖然 Go 沒有提供多進程編程,但啓動 Go 程序還是會有一個進程存在的,Go 標準庫提供了 os.Getpid 函數,可以方便的獲取當前進程的 id:
https://github.com/jianghushinian/blog-go-example/blob/main/goroutine/id/pid/main.go
package main
import (
"fmt"
"os"
)
func main() {
// 獲取當前進程的 id
pid := os.Getpid()
fmt.Println("process id:", pid)
}
調用 os.Getpid() 會返回當前進程的 pid(進程 id)。
獲取當前 goroutine 的 id
Go 並沒有直接提供獲取 goroutine id 的方法,因爲 goroutine 的管理是由 Go 運行時(Go runtime)負責的,它並不暴露每個 goroutine 的 id。然而,有一些方法可以間接獲取到與 goroutine 相關的信息。
使用 runtime 包獲取 goroutine id
雖然不能直接獲取每個 goroutine 的 id,但我們可以變相的通過 runtime.Stack 函數來獲取。
實現代碼如下:
https://github.com/jianghushinian/blog-go-example/blob/main/goroutine/id/main.go
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
"sync"
)
func GoId() int {
buf := make([]byte, 32)
n := runtime.Stack(buf, false)
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
id, err := strconv.Atoi(idField)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func main() {
fmt.Println("main", GoId())
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, GoId())
}()
}
wg.Wait()
}
這段代碼通過自定義的 GoId 函數來獲取當前 goroutine 的 id,並在 main 函數的主 goroutine 和子 goroutines 中打印它們的 id。
我們來解釋下 GoId 函數主要邏輯的實現:
runtime.Stack(buf, false):
-
runtime.Stack是runtime包提供的公開函數,用於獲取當前 goroutine 的堆棧信息。 -
參數
buf是一個字節數組,用來存儲調用runtime.Stack()返回的堆棧信息。 -
第二個參數是
false,表示我們只獲取當前 goroutine 的棧信息,如果爲true則是獲取所有 goroutines 的棧信息。 -
runtime.Stack()會將當前 goroutine 的棧信息寫入buf中,n是返回的字節數,表示堆棧信息的長度。
-
strings.TrimPrefix(string(buf[:n]), "goroutine "):goroutine 1 [running]:
-
這裏通過
TrimPrefix去除前綴"goroutine "後,剩下的內容就是 goroutine 的 id 及其狀態信息。 -
將堆棧信息轉爲字符串並去掉前綴
"goroutine "。 -
堆棧信息的第一行格式通常如下:
idField := strings.Fields(...)[0]:
-
strings.Fields將經過TrimPrefix處理後的字符串按空格切割成一個字符串切片。 -
從切片中獲取第一個字段,這就是 goroutine 的 id,如
1。
如果一切順利,GoId 函數最終返回當前 goroutine 的 id。
main 函數實現則比較簡單,先調用 GoId() 打印主 goroutine 的 id,然後啓動了 10 個子 goroutine 並分別打印它們的 id。
執行示例代碼,得到如下輸出:
$ go run main.go
main 1
9 29
0 20
5 25
6 26
7 27
8 28
2 22
1 21
4 24
3 23
這樣,我們就變相的通過先獲取堆棧信息,然後再從堆棧信息中進行解析的方式拿到了 goroutine 的 id。想必你也能夠發現,這種實現方式性能不高,所以不到萬不得已,不要輕易獲取 goroutine 的 id。
那麼有沒有更高效的方式呢?
很遺憾,Go 官方沒有提供。不過有第三方庫幫我們實現了。
使用第三方庫獲取 goroutine id
一個比較常用的庫是 https://github.com/petermattis/goid,可以用來獲取當前 goroutine 的 id。
安裝方式:
$ go get github.com/petermattis/goid
使用示例:
https://github.com/jianghushinian/blog-go-example/blob/main/goroutine/id/goid/main.go
package main
import (
"fmt"
"sync"
"github.com/petermattis/goid"
)
func main() {
fmt.Println("main", goid.Get())
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, goid.Get())
}()
}
wg.Wait()
}
執行示例代碼,得到如下輸出:
$ go run goid/main.go
main 1
9 43
4 38
5 39
6 40
7 41
8 42
1 35
0 34
2 36
3 37
我們僅需要調用 goid.Get() 即可獲取當前 goroutine 的 id。
goid 庫使用了 C 和 彙編來獲取 goroutine id,所以性能更好。並且 goid 爲所有的 Go 版本都做了兼容,從項目文件名可以看出,不同 Go 版本有着不同的實現:
$ tree goid
goid
├── LICENSE
├── README.md
├── go.mod
├── goid.go
├── goid_gccgo.go
├── goid_go1.3.c
├── goid_go1.3.go
├── goid_go1.4.go
├── goid_go1.4.s
├── goid_go1.5.go
├── goid_go1.5.s
├── goid_slow.go
├── goid_test.go
├── runtime_gccgo_go1.8.go
├── runtime_go1.23.go
├── runtime_go1.5.go
├── runtime_go1.6.go
└── runtime_go1.9.go
1 directory, 18 files
在 goid_go1.3.c 中可以看到 C 語言版本實現如下:
https://github.com/petermattis/goid/blob/master/goid_go1.3.c
// +build !go1.4
#include <runtime.h>
void ·Get(int64 ret) {
ret = g->goid;
USED(&ret);
}
在 goid_go1.4.s 中可以看到彙編語言版本實現如下:
https://github.com/petermattis/goid/blob/master/goid_go1.4.s
// +build amd64 amd64p32 arm 386
// +build go1.4,!go1.5
#include "textflag.h"
#ifdef GOARCH_arm
#define JMP B
#endif
TEXT ·getg(SB),NOSPLIT,$0-0
JMP runtime·getg(SB)
此外,爲了保證兼容性,在 goid.go 中還有一個 Go 語言版本實現:
https://github.com/petermattis/goid/blob/master/goid.go
package goid
import (
"bytes"
"runtime"
"strconv"
)
func ExtractGID(s []byte) int64 {
s = s[len("goroutine "):]
s = s[:bytes.IndexByte(s, ' ')]
gid, _ := strconv.ParseInt(string(s), 10, 64)
return gid
}
// Parse the goid from runtime.Stack() output. Slow, but it works.
func getSlow() int64 {
var buf [64]byte
return ExtractGID(buf[:runtime.Stack(buf[:], false)])
}
這裏 Go 版本的實現同樣使用 runtime.Stack(),並且註釋也標明瞭這個實現比較慢。
所以,如果我們真的需要獲取 goroutine 的 id,那麼推薦使用 goid。
總結
在 Go 中獲取當前進程的 id 可以使用 os.Getpid() 函數。如果要獲取當前 goroutine 的 id 則要困難一些,Go 標準庫沒有直接提供該功能,不過我們可以變相的從 runtime.Stack() 返回的堆棧信息中獲取,也可以使用第三方庫 goid 來獲取。
不過,你有沒有想過,爲什麼 Go 不提供獲取當前 goroutine 的 id 的功能呢?
這個問題早有討論,其實在早期的 Go 版本中提供了這樣的方法,只不過後來被取消了。討論過程你可以在這裏 https://groups.google.com/g/golang-nuts/c/Nt0hVV_nqHE 看到。
總結起來就是 Go 設計故意不讓用戶獲取 goroutine 的 id,避免濫用。所以,要不要在你的代碼中使用 goroutine 的 id,你需要自定判斷。
本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。
希望此文能對你有所啓發。
延伸閱讀
-
Re: [go-nuts] How to get current goroutine id?:https://groups.google.com/g/golang-nuts/c/Nt0hVV_nqHE
-
How can I get a goroutine's runtime ID?:https://stackoverflow.com/questions/75361134/how-can-i-get-a-goroutines-runtime-id
-
如何得到 goroutine 的 id?:https://colobu.com/2016/04/01/how-to-get-goroutine-id/
-
goid GitHub 源碼:https://github.com/petermattis/goid/
-
runtime.Stack Documentation:https://pkg.go.dev/runtime@go1.23.0#Stack
-
本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/goroutine/id
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
-
GitHub:https://github.com/jianghushinian
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KTF2GyEw1JLq0MX3bNv9Ig