Go语言如何获取goroutine的id

 更新时间:2024年12月09日 08:31:30   作者:江湖十年  
在Go语言中,获取 goroutine的id并不像其他编程语言那样容易,但依然有办法,这篇文章就来和大家聊聊具体实现的方法,感兴趣的小伙伴可以了解下

如果你使用过如 Python、Java 等主流支持并发的编程语言,那么通常都能够比较容易的获得进程和线程的 id。但是在 Go 语言,没有直接提供对多进程和多线程的支持,而是提供了 goroutine 来支持并发编程。不过在 Go 中,获取 goroutine 的 id 并不像其他编程语言那样容易,但依然有办法,本文就来介绍下如何实现。

获取当前进程的 id

首先,虽然 Go 没有提供多进程编程,但启动 Go 程序还是会有一个进程存在的,Go 标准库提供了 os.Getpid 函数,可以方便的获取当前进程的 id:

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 函数来获取。

实现代码如下:

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

  • runtime.Stackruntime 包提供的公开函数,用于获取当前 goroutine 的堆栈信息。
  • 参数 buf 是一个字节数组,用来存储调用 runtime.Stack() 返回的堆栈信息。
  • 第二个参数是 false,表示我们只获取当前 goroutine 的栈信息,如果为 true 则是获取所有 goroutines 的栈信息。
  • runtime.Stack() 会将当前 goroutine 的栈信息写入 buf 中,n 是返回的字节数,表示堆栈信息的长度。

2.strings.TrimPrefix(string(buf[:n]), "goroutine "):

goroutine 1 [running]:
  • 将堆栈信息转为字符串并去掉前缀 "goroutine "
  • 堆栈信息的第一行格式通常如下:
  • 这里通过 TrimPrefix 去除前缀 "goroutine " 后,剩下的内容就是 goroutine 的 id 及其状态信息。

3.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

一个比较常用的库是 github.com/petermattis/goid,可以用来获取当前 goroutine 的 id。

安装方式:

$ go get github.com/petermattis/goid

使用示例:

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 语言版本实现如下:

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 中可以看到汇编语言版本实现如下:

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 语言版本实现:

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获取goroutine的id的资料请关注脚本之家其它相关文章!

相关文章

  • 浅析go语言如何实现协程的抢占式调度的

    浅析go语言如何实现协程的抢占式调度的

    go语言通过GMP模型实现协程并发,为了避免单协程持续持有线程导致线程队列中的其他协程饥饿问题,设计者提出了一个抢占式调度机制,本文会基于一个简单的代码示例对抢占式调度过程进行深入讲解剖析
    2024-04-04
  • golang copy函数使用的坑

    golang copy函数使用的坑

    本文主要介绍了golang copy函数使用的坑,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • go语言之给定英语文章统计单词数量(go语言小练习)

    go语言之给定英语文章统计单词数量(go语言小练习)

    这篇文章给大家分享go语言小练习给定英语文章统计单词数量,实现思路大概是利用go语言的map类型,以每个单词作为关键字存储数量信息,本文通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
    2020-01-01
  • golang中new与make的区别讲解

    golang中new与make的区别讲解

    new只能开辟单个空间,不能为引用类型开辟多个空间,并且new是对类型进行内存的开辟,返回一个指向该内存空间的指针类型,如果使用new去初始化引用数据类型,不是很合适(当然,new一个对象还是可以的),因此就需要用到另一个内置函数make,需要的朋友可以参考下
    2023-01-01
  • Go Slice扩容的这些坑你踩过哪些

    Go Slice扩容的这些坑你踩过哪些

    这篇文章主要为大家详细介绍了Golang中对切片Slice的append操作时会遇到的踩坑经验分享,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-03-03
  • Golang测试框架goconvey进行单元测试流程介绍

    Golang测试框架goconvey进行单元测试流程介绍

    goconvey是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多Web界面特性,这篇文章主要介绍了使用goconvey进行单元测试流程,感兴趣的同学可以参考下文
    2023-05-05
  • Golang sync包中errgroup的使用详解

    Golang sync包中errgroup的使用详解

    Go 语言为我们提供了丰富的并发原语,且大多数都位于 sync 包下,今天我们来探讨一下该库下的原语之一:errgroup,感兴趣的小伙伴可以了解一下
    2023-05-05
  • go实现base64编码的四种方式

    go实现base64编码的四种方式

    本文主要介绍了go实现base64编码的四种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 基于context.Context的Golang loader缓存请求放大问题解决

    基于context.Context的Golang loader缓存请求放大问题解决

    这篇文章主要为大家介绍了基于context.Context的Golang loader缓存请求放大解决方案,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • go语言gin框架中间件详解

    go语言gin框架中间件详解

    在Go语言中,gin是一个常用的Web框架,用于构建RESTful API和Web应用程序。本文将通过代码示例详细介绍了gin框架中间件,感兴趣的同学可以参考阅读
    2023-04-04

最新评论