在 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 函數主要邏輯的實現:

  1. runtime.Stack(buf, false)
  1. strings.TrimPrefix(string(buf[:n]), "goroutine ")

    goroutine 1 [running]:
  1. idField := strings.Fields(...)[0]

如果一切順利,GoId 函數最終返回當前 goroutine 的 id。

main 函數實現則比較簡單,先調用 GoId() 打印主 goroutine 的 id,然後啓動了 10 個子 goroutine 並分別打印它們的 id。

執行示例代碼,得到如下輸出:

$ go run main.go
main 129202526272822212423

這樣,我們就變相的通過先獲取堆棧信息,然後再從堆棧信息中進行解析的方式拿到了 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 143383940414235343637

我們僅需要調用 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([]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 中,歡迎點擊查看。

希望此文能對你有所啓發。

延伸閱讀

聯繫我

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KTF2GyEw1JLq0MX3bNv9Ig