没错,这就是之前介绍过的 C 调用 Go 快十倍的优化,也是 很快被打脸 的后续。
算上第一次被打脸,这个补丁一共被踢出来三次,好在第四次合入之后,目前已经进入了 Go 1.21 rc1 版本,后续再被踢出来的概率应该比较低了。
第一次被踢
回顾下第一次被踢,简单说,是因为 g0 栈的 stack.lo
只是在当前栈顶往下多留了 32k
,只适用于当前这一次 cgo 调用,后续的 cgo 调用,栈顶位置可能已经超出 stack.lo
了,就直接栈溢出了。
Cherry 大佬的修复方案是,将 stack.lo
设置为当前 C 栈的栈顶,也就是 g0 的栈顶,直接跟 C 栈的栈顶对齐了,也就是 g0 的 stack.lo
直接拉到最低。
具体细节,这里就不做过多介绍了,可以看 之前打脸的文章。
第二次被踢
第二次是因为 TSAN 检测到了 race,具体情况是:
- 从 C 的角度来看:不同的 C 线程在获取 M 的时候,有可能会竞争写同一个 M 的
M1.g0.stack.lo
,导致 race。 - 但是呢,TSAN 只分析了 C 代码,并不知道在 Go 侧获取 M 是有锁的
所以,这是实际上是 TSAN 的一个误报,具体细节可以看 Michael Pratt 大佬的分析:
https://github.com/golang/go/issues/59678#issuecomment-1512114382
解决办法是,不再传 g0 的地址到 C 了,而是传一个 stack.lo
的地址给 x_cgo_getstackbound
这个 C 函数,这样 C 侧看到的就是两个地址了,就没法分析竞争了,也就不会误报了。
具体可以见这个 CL:
https://go-review.googlesource.com/c/go/+/485500
第三次被踢
第二次被踢还是有点无辜,第三次被踢则是真的有两个 bug:
Go 线程数限制
Go 默认是有最大 10k 线程数的限制,是通过 M 的计数来实现的。
不过原来 extra M 也是计算在内的,因为我们的 extra M 会被 C 线程绑定,这会导致一种情况,如果有 10 k 个 C 线程绑定了 extra M,那么 M 数量直接就到了 10 k 了。
这里的解决方案是,计算 Go 线程数量的时候,extra M 不计算在内,这样就只限制 Go 线程的数量,C 线程的数量,Go 本来也控制不了的。
具体可以见 Michael Pratt 在这里的描述:
https://github.com/golang/go/issues/60004
stack.hi
第一次踢出来是因为 stack.lo
栈顶,那是在 Go 在动态检测是否需要扩栈的时候用到的。
这次是则是因为 stack.hi
栈底,这个则是在一个比较冷门的地方用到的,在 Go 收到系统信号的时候,就有一处判断 adjustSignalStack
是,当前栈帧 sp
,是否在 Go stack 中,也就是 sp >= stack.lo && sp < stack.hi
,来判断是否为来自非 Go 代码的触发。
具体可以见 Michael Pratt 在这里的描述:
https://github.com/golang/go/issues/60007
关于 cgo 场景下 Go 对于信号的处理,确实是比较绕的,之前也写过一篇文章介绍过:
https://uncledou.site/2022/go-cgo-c-to-go/
解决方案跟 stack.lo
也是类似的,也就是在 C 侧直接将 stack.hi
拉到最高,跟 C 栈的栈底对齐。
具体修复在这里:
https://go-review.googlesource.com/c/go/+/495855/
最后
相对来说,后面两次被踢信息量没那么大,还是 cgo 场景下,g0 这个特殊 goroutine 的栈空间问题。
最后,希望不要再被踢出来了… 确实也是够折腾的…
如果你也有 cgo 的场景,欢迎联系,一起交流学习