获取Goroutine Id的最佳实践

序言

在C/C++/Java等语言中,我们可以直接获取Thread Id,然后通过映射Thread Id和二级调度Task Id的关系,可以在日志中打印当前的TaskId,即用户不感知Task Id的打印,适配层统一封装,这使得多线程并发的日志的查看或过滤变得非常容易。

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

Goroutine非常亮眼,但是自从go1.4版本以后,Goroutine Id无法直接从Go Runtime获取了。

这是Golang的开发者故意为之,避免开发者滥用Goroutine Id实现Goroutine Local Storage(类似java的Thread Local Storage), 因为Goroutine Local Storage很难进行垃圾回收。因此尽管Go1.4之前暴露出了相应的方法,现在已经把它隐藏了。

这个决策有点因噎废食,对于高并发日志的查看和过滤就变得比较困难。尽管在日志中可以使用业务本身的Id,但是在很多函数中仅仅为了打印而增加一些入参对于追求Clean Code的程序员实在无法接受。

笔者在本文中将找出一种简单高效稳定的解决方法,并给出最佳实践。

既有的几种方法

通过汇编获取

复杂度高,偏移地址随版本可能有变化,不建议使用

通过第三方库获取

相关的第三方库可以在github上找,比如:

https://github.com/jtolds/gls
https://github.com/huandu/goroutine

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

通过runtime.Stack获取

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

采用该方法时,Goid函数的实现如下:

func Goid() int {
    defer func()  {
        if err := recover(); err != nil {
            fmt.Println("panic recover:panic info:%v", err)     }
    }()

    var buf [64]byte
    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
}

通过修改编译器源码获取

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

该方法将在“最佳实践”一节中详述。

方法三和方法四比较

分别采用方法三和方法四,将Goid函数连续调用10000次的性能数据如下:

方法三 方法四
> 50ms < 5us

对于方法三,获取堆栈信息会影响性能,所以建议对性能不敏感的场景采用;
对于方法四,直接调用runtime的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:

func Goid() int64 {
    _g_ := getg()
    return _g_.goid
}

运行src/make.bash命令(默认使用$HOME/go1.4目录下的编译器),编译go1.7.3的新版本。

编译完成后,将go文件夹拷贝到GOROOT目录下,使之生效:

$ go version
go version go1.7.3 linux/amd64

测试代码

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

type Vector []int

func (v Vector) DoSome(i, n int, u Vector, c chan int, add *int) int {
    for ; i < n; i++ {
        *add += u[i]
    }
    id := runtime.Goid(id)
    fmt.Println("id:", id)
    c <- 1
    return 1
}

const NCPU = 16

func (v Vector) DoAll(u Vector) int {
    c := make(chan int, NCPU)
    var add [NCPU]int
    sum := 0
    for i := 0; i < NCPU; i++ {
        go v.DoSome(i * len(v) / NCPU, (i + 1)* len(v) / NCPU, u, c, &add[i])
    }

    for i := 0; i < NCPU; i++ {
        <- c
    }
    for i := 0; i < NCPU; i++ {
        sum += add[i]
    }
    return sum
}

func main() {
    x := 0
    y := 0
    v := make(Vector, 160)
    for i := 0; i < 160; i++ {
        v[i] = i
        x += i
    }
    y = v.DoAll(v)
    fmt.Println("x =", x, "and y =", y)
}

日志

通过查看日志,我们已将成功获取到了Goroutine Id。一个字,完美!

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

适配层封装

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

小结

本文针对Golang中Goroutine的高并发的日志难以查看或过滤的问题,分析了既有的几种获取Goroutine Id的方法,最后找到一种简单高效稳定的方法,即通过修改编译器源码获取,并给出了最佳实践,希望对读者有一定的帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 迁移自CSDN:http://blog.csdn.net/erlib/article/details/518509...
    Sunface撩技术阅读 2,274评论 0 10
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 我们面临怎样的挑戓? •海量用户请求,预计峰值3w/秒 对应策略:万一超过怎么办:过载保护:前端保护后端,后端拒...
    jlliubo阅读 339评论 0 0