Goroutine是Golang中轻量级线程的实现,由Go Runtime管理。Golang在语言级别支持轻量级线程,叫做协程。Golang标准库提供的所有系统调用操作,都会出让CPU给其他Goroutine。这让事情变得非常简单,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。

Goroutine非常亮眼,但是自从go1.4版本以后,GoroutineId无法直接从Go Runtime获取了。这是Golang的开发者故意为之,避免开发者滥用Goroutine Id实现Goroutine Local Storage,因为Goroutine Local Storage很难进行垃圾回收。

通过汇编获取

  1. 复杂度高,便宜地址随版本可能有变化,不建议使用

通过第三方库获取

相关第三方库可以在github上找,比如:
https://github.com/jtolds/gls
https://github.com/huandu/goroutine

稳定性未知,性能也不高,不建议使用

通过runtime.Stack获取

它利用runtime.Stack的堆栈信息,将当前的堆栈信息写入到一个slice中,堆栈的第一行为”goroutine ###[…”,其中”###”就是当前的Goroutine Id,通过这个方法就可以实现Goid函数了。

Goid函数的实现如下:

  1. func Goid() int {
  2. defer func() {
  3. if err := recover(); err != nil {
  4. fmt.Println("panic recover:panic info:%v", err) }
  5. }()
  6. var buf [64]byte
  7. n := runtime.Stack(buf[:], false)
  8. idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
  9. id, err := strconv.Atoi(idField)
  10. if err != nil {
  11. panic(fmt.Sprintf("cannot get goroutine id: %v", err))
  12. }
  13. return id
  14. }

通过修改编译器源码获取

在go源码runtime包中增加函数Goid,直接调用runtine的getg函数获取,具有简单高校稳定的优点,同时每个团队可以通过容器来部署自己的微服务。

最佳实践

下载go1.4版本的编译器
在Golang的官方网站下载go1.4版本的编译器,URL如下:
https://golang.org/dl/
解压缩,将go文件夹rename成go1.4,然后移动到$HOME目录下。

修改go1.7.3版本的编译器代码
在Golang的官方网站下载go1.7.3版本的源码。编辑src/runtime/proc.go文件,在尾部添加函数Goid:

  1. func Goid() int64 {
  2. _g_ := getg()
  3. return _g_.goid
  4. }

运行src/make.bash命令(默认使用$HOME/go1.4目录下编译器),编译go1.7.3的新版本。编译完成后,将go文件夹拷贝到GOROOT目录下,使之生效:

  1. $ go version
  2. go version go1.7.3 linux/amd64

测试代码
我们模拟一个完全可以并行的计算任务:计算N个整型数的总和。我们可以将所有整型数分成M份,M即CPU的个数。让每个CPU开始计算分给它的那份计算任务,最后将每个CPU的计算结果再做一次累加,这样就可以得到所有N个整型数的总和,实现代码如下:

  1. type Vector []int
  2. func (v Vector) DoSome(i, n int, u Vector, c chan int, add *int) int {
  3. for ; i < n; i++ {
  4. *add += u[i]
  5. }
  6. id := runtime.Goid(id)
  7. fmt.Println("id:", id)
  8. c <- 1
  9. return 1
  10. }
  11. const NCPU = 16
  12. func (v Vector) DoAll(u Vector) int {
  13. c := make(chan int, NCPU)
  14. var add [NCPU]int
  15. sum := 0
  16. for i := 0; i < NCPU; i++ {
  17. go v.DoSome(i * len(v) / NCPU, (i + 1)* len(v) / NCPU, u, c, &add[i])
  18. }
  19. for i := 0; i < NCPU; i++ {
  20. <- c
  21. }
  22. for i := 0; i < NCPU; i++ {
  23. sum += add[i]
  24. }
  25. return sum
  26. }
  27. func main() {
  28. x := 0
  29. y := 0
  30. v := make(Vector, 160)
  31. for i := 0; i < 160; i++ {
  32. v[i] = i
  33. x += i
  34. }
  35. y = v.DoAll(v)
  36. fmt.Println("x =", x, "and y =", y)
  37. }

日志
通过查看日志,我们已将成功获取到了Goroutine Id。

  1. id: 20
  2. id: 13
  3. id: 7
  4. id: 12
  5. id: 14
  6. id: 9
  7. id: 5
  8. id: 17
  9. id: 16
  10. id: 10
  11. id: 6
  12. id: 15
  13. id: 18
  14. id: 19
  15. id: 8
  16. id: 11
  17. x = 12720 and y = 12720

适配层封装
我们可以将glog等第三方库的日志接口进行简单封装,隐藏goid的获取和打印过程,使得用户轻松。


Golang获取Goroutine Id的最佳实践 - 图1