C扩展模块在多线程下崩溃,大概率是C代码绕过GIL且未加同步机制所致;GIL仅保护Python字节码,C中释放GIL后若共享内存无锁保护,将引发数据竞争、SIGSEGV等错误。

直接结论:C扩展模块在多线程下崩溃,大概率不是GIL没起作用,而是你写的C代码绕过了GIL保护,又没加自己的同步机制。
为什么GIL不拦得住你的C扩展崩溃
GIL只管Python字节码执行,一旦C扩展调用耗时计算、I/O或显式释放GIL(比如用 Py_BEGIN_ALLOW_THREADS),线程就自由了——此时若多个线程同时读写同一块C内存(如全局静态变量、共享缓冲区、单例结构体),就会触发数据竞争。典型表现是随机 SIGSEGV、double-free 或内存值被意外覆盖。
- NumPy数组计算、OpenCV图像处理、自定义加密函数等场景常主动释放GIL
- 检查你的C代码里是否有
Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS成对出现 - 用
py-spy record -p $(pidof python) --duration 10看热点是否卡在你的C函数内,而非Python层
给C扩展加锁的三种实际做法
别只靠Python层的 threading.Lock——它进不了C函数内部。必须在C侧用原生同步原语。
- 用
pthread_mutex_t保护共享资源:声明为static pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER,在读写前pthread_mutex_lock(&my_mutex),结束后pthread_mutex_unlock(&my_mutex) - 避免锁粒度太粗:不要整个函数加锁,只锁真正共享的变量或内存段;否则会抵消多线程收益
- 慎用
static全局变量:它跨线程可见,但生命周期与模块加载绑定,重启解释器也不重置;考虑改用线程局部存储(__thread或pthread_getspecific)
排查引用计数异常导致的崩溃
多线程下漏调 Py_INCREF 或多调 Py_DECREF 会导致对象提前释放,另一线程再访问就崩。这不是竞态,是逻辑错误,但常和多线程一起暴露。
立即学习“Python免费学习笔记(深入)”;
- 所有从Python API返回的指针(如
PyObject_GetAttrString、PyList_GetItem)默认不增加引用计数,需手动Py_INCREF - 用
PYTHONDEVMODE=1运行,会捕获负引用计数和非法释放,并打印详细位置 - 在C函数入口加
if (!Py_IsInitialized()) return NULL;,防止模块被多解释器并发加载时状态错乱
测试时必须隔离子进程
一个崩溃的C扩展会让整个Python进程退出,没法继续跑后续测试。别在同一个进程中反复 import + 调用。
- 用
subprocess.run([sys.executable, "-c", "import myext; myext.risky_func()"], timeout=10)单独拉起进程 - 检查返回码:
result.returncode == -11基本就是段错误;== -6是 abort,常因引用计数异常触发 - 配合
gdb --args python -c "import myext; myext.risky_func()"直接定位到C源码行,比看Python traceback有用得多
真正麻烦的不是加锁本身,而是判断哪些变量该被锁——C扩展里一个没文档的静态缓存、一个没声明为 thread_local 的指针,都可能成为定时炸弹。上线前务必用 tsan(ThreadSanitizer)编译C代码跑压测,它比人眼靠谱得多。


















