TDengine Go 连接器 https://github.com/taosdata/driver-go 使用 cgo 调用 taos.so 中的 API,使用过程中发现线程数不断增长,本文从一个 cgo 调用开始解析 Go 源码,分析造成线程增长的原因。
转换 cgo 代码
对 driver-go/wrapper/taosc.go 进行转换
go tool cgo taosc.go
执行后生成 _obj
文件夹
go 代码分析
以 taosc.cgo1.go
中 TaosResetCurrentDB
为例来分析。
// TaosResetCurrentDB void taos_reset_current_db(TAOS *taos);
func TaosResetCurrentDB(taosConnect unsafe.Pointer) {
func() { _cgo0 := /*line :161:26*/taosConnect; _cgoCheckPointer(_cgo0, nil); _Cfunc_taos_reset_current_db(_cgo0); }()
}
//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
func _cgoCheckPointer(interface{}, interface{})
//go:cgo_unsafe_args
func _Cfunc_taos_reset_current_db(p0 unsafe.Pointer) (r1 _Ctype_void) {
_cgo_runtime_cgocall(_cgo_453a0cad50ef_Cfunc_taos_reset_current_db, uintptr(unsafe.Pointer(&p0)))
if _Cgo_always_false {
_Cgo_use(p0)
}
return
}
//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32
//go:cgo_import_static _cgo_453a0cad50ef_Cfunc_taos_reset_current_db
//go:linkname __cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db _cgo_453a0cad50ef_Cfunc_taos_reset_current_db
var __cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db byte
var _cgo_453a0cad50ef_Cfunc_taos_reset_current_db = unsafe.Pointer(&__cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db)
TaosResetCurrentDB
首先调用_cgoCheckPointer
检查传入参数是否为nil
。//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
表示cgoCheckPointer
方法实现是runtime.cgoCheckPointer
,如果传入参数是nil
程序将会panic
。- 接着调用
_Cfunc_taos_reset_current_db
。 Cfunc_taos_reset_current_db
方法中_Cgo_always_false
在运行时会是 false,所以只分析第一句_cgo_runtime_cgocall(_cgo_453a0cad50ef_Cfunc_taos_reset_current_db, uintptr(unsafe.Pointer(&p0)))
。_cgo_runtime_cgocall
实现是runtime.cgocall
这个会重点分析。_cgo_453a0cad50ef_Cfunc_taos_reset_current_db
由上方最后代码块可以看出是taos_reset_current_db
方法指针。uintptr(unsafe.Pointer(&p0))
表示 p0 的指针地址。- 由上面可以看出这句意思是调用
runtime.cgocall
,参数为方法指针和参数的指针地址。
分析 runtime.cgocall
基于 golang 1.20.4
分析该方法
func cgocall(fn, arg unsafe.Pointer) int32 {
if !iscgo && GOOS != "solaris" && GOOS != "illumos" && GOOS != "windows" {
throw("cgocall unavailable")
}
if fn == nil {
throw("cgocall nil")
}
if raceenabled {
racereleasemerge(unsafe.Pointer(&racecgosync))
}
mp := getg().m // 获取当前 goroutine 的 M
mp.ncgocall++ // 总 cgo 计数 +1
mp.ncgo++ // 当前 cgo 计数 +1
mp.cgoCallers[0] = 0 // 重置追踪
entersyscall() // 进入系统调用,保存上下文, 标记当前 goroutine 独占 m, 跳过垃圾回收
osPreemptExtEnter(mp) // 标记异步抢占, 使异步抢占逻辑失效
mp.incgo = true // 修改状态
errno := asmcgocall(fn, arg) // 真正进行方法调用的地方
mp.incgo = false // 修改状态
mp.ncgo-- // 当前 cgo 调用-1
osPreemptExtExit(mp) // 恢复异步抢占
exitsyscall() // 退出系统调用,恢复调度器控制
if raceenabled {
raceacquire(unsafe.Pointer(&racecgosync))
}
// 避免 GC 过早回收
KeepAlive(fn)
KeepAlive(arg)
KeepAlive(mp)
return errno
}
其中两个主要的方法 entersyscall
和 asmcgocall
,接下来对这两个方法进行着重分析。
分析 entersyscall
func entersyscall() {
reentersyscall(getcallerpc(), getcallersp())
}
entersyscall
直接调用的 reentersyscall
,关注下 reentersyscall
注释中的一段:
// If the syscall does not block, that is it, we do not emit any other events.
// If the syscall blocks (that is, P is retaken), re