go 中调用 c 函数不会阻塞整个 p,运行时通过 sysmon 检测超 20 微秒的阻塞并解绑线程,动态腾挪资源保障其他 goroutine 并发执行。

Go 的 goroutine 能和 C 代码并行运行,但不是“自动无感协同”,而是靠 Go 运行时主动隔离、补偿和调度——只要 C 函数不长期独占线程,其他 goroutine 就不会被卡住。
goroutine 调用 C 函数时是否阻塞整个 P?
不会。即使 GOMAXPROCS=1,调用 C.usleep(1000000)(1 秒)也不会让其他 goroutine 饿死。原因在于:Go 运行时在检测到 C 调用耗时超过约 20 微秒后,会触发线程解绑(lockedToThread = false),允许新线程启动其他 goroutine。
这意味着:
- 单个 C 调用只短暂绑定一个 M(OS 线程),不长期霸占 P 的调度配额
- sysmon 监控协程会周期性扫描阻塞的 M,并催生新 M 来接管就绪 G
- 你看到的“并发执行”不是假象,是运行时在底层动态腾挪资源的结果
为什么有时 goroutine 像被卡住了?
常见于以下几种情况,本质都是 C 侧行为超出了运行时的补偿能力:
-
C.fork()或C.pthread_create()后未正确pthread_detach(),导致线程泄漏,最终耗尽系统线程数 - C 函数内部调用了不可重入或信号阻塞的系统调用(如某些版本的
gethostbyname),引发整个 M 挂起且无法被 sysmon 及时识别 - 手动调用
runtime.LockOSThread()后进入 C 代码,又没在 C 里调用pthread_exit或等价逻辑,导致该 M 永久锁定,无法回收 - 在 CGO_ENABLED=0 环境下强制调用 cgo(如交叉编译 iOS/Android 时未关掉 cgo),直接 panic 报
undefined: C.xxx
C 代码里 spawn 新线程会影响 Go 调度吗?
不影响 Go 的 G-M-P 调度模型本身,但会影响资源可见性与调试复杂度:
- 由 C 主动创建的 pthread 不受 Go 运行时管理,它们不会计入
GOMAXPROCS,也不参与 goroutine 抢占调度 - 这些线程若调用 Go 导出的函数(如
export MyGoFunc),必须先调用runtime.cgocall()入口,否则可能破坏栈寄存器状态 - 若 C 线程长期运行且频繁调用 Go 函数,会增加 STW(stop-the-world)期间的等待时间,因为运行时需确保所有 C 关联线程都处于安全点
- 调试时,
pprof默认不采集 C 线程 CPU 样本,需额外加// #cgo CFLAGS: -pg并用gprof配合分析
实际写法中哪些细节最容易翻车?
真正出问题的往往不是“能不能跑”,而是生命周期和所有权错位:
- 用
C.CString分配的内存,必须在 C 函数返回后、且确定不再被任何 C 线程访问时,才调用C.free;提前 free 会导致 use-after-free - 把
*C.char传给多个 goroutine 并发读,而 C 侧内存由单个 C 函数栈分配(比如return buf;),就会读野指针 - 在 C 回调函数里直接调用 Go 函数并试图
panic,会触发fatal error: call of panic on system stack,因 C 栈上无 Go panic 支持 - 跨 goroutine 传递含
unsafe.Pointer的 struct 时,没用runtime.KeepAlive延长 Go 对象生命周期,GC 可能在 C 还没用完前就回收了底层数组
最常被忽略的一点:C 函数的「阻塞」不是二值判断,而是一个连续谱——从纳秒级 inline 汇编,到毫秒级 usleep,再到秒级 read 等待磁盘 IO,运行时的应对策略完全不同;不能只看“有没有输出”,得用 go tool trace 观察 Goroutine 的 block/probe 事件分布。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











