Python threading.Thread 的底层实现原理

2次阅读

pythonThreading.thread并非os线程封装,受gil限制,计算密集型任务无法并发;需用multiprocessing或asyncio绕过gil;start()才是合法启动入口,run()直接调用等同同步执行。

Python threading.Thread 的底层实现原理

Python 的 threading.Thread 不是操作系统线程的直接封装

它底层确实调用 pthread_createlinux/macos)或 CreateThreadwindows),但中间隔着 CPython 的 PyThread_start_new_thread 和 GIL 管理逻辑。这意味着你启动一个 threading.Thread,OS 层面会多一个线程,但 Python 字节码执行仍受 GIL 排他锁制约。

常见错误现象:time.sleep(1)多线程里“看起来”并发,但纯计算密集型任务(比如 sum(range(10**7)))几乎不提速——因为 GIL 始终只放行一个线程执行 Python 字节码。

  • 真正释放 GIL 的操作:I/O(read/write/recv)、部分 C 扩展(如 numpy 数组运算)、显式调用 time.sleep()
  • 想绕过 GIL?得用 multiprocessingasyncio + 非阻塞 I/O,而不是更多 threading.Thread
  • 注意:threading.Thread.daemon=True 的线程会在主线程退出时被强制终止,不会等待其自然结束——这常导致日志没刷盘、临时文件没清理

start()run() 的区别不是语义问题,是执行时机问题

start() 是唯一合法的线程启动入口;它负责注册线程、触发 OS 创建、调度进 GIL 等待队列。run() 只是一个普通方法,直接调用等价于同步执行——根本没开新线程。

常见错误现象:写 t.run() 代替 t.start(),结果所有逻辑串行跑完,还误以为“多线程跑完了”。调试时加 print(threading.current_thread().name) 就能立刻暴露问题。

立即学习Python免费学习笔记(深入)”;

  • 永远只调 start();重写 run() 来定义线程体,别重写 start()
  • start() 只能调一次,重复调用抛 RuntimeError: threads can only be started once
  • 启动后立即查 t.is_alive() 可能返回 False——因为线程刚创建还没来得及进入运行态,建议用 t.join(timeout=0.1)事件机制做协调

GIL 导致的竞态条件比想象中更隐蔽

很多人以为“没共享可变对象就安全”,但 Python 中一些看似原子的操作其实不是:比如 list.append() 是原子的,但 counter += 1(等价于 counter = counter + 1)包含读-算-写三步,必然被 GIL 中断。

使用场景:多个线程更新同一个 dict 的计数器字段、往同一个 list 追加元素、修改类实例属性。

  • 别依赖“小操作很短所以不会被打断”——GIL 切换点不只在字节码边界,也发生在循环计数器溢出、信号到达等时刻
  • 最轻量的修复是用 threading.Lock 包住临界区;更推荐用线程安全类型,比如 queue.Queue 替代 listthreading.local() 隔离线程状态
  • threading.RLock 允许同一线程多次 acquire,适合递归调用场景,但性能略低,别无脑替换 Lock

线程生命周期和资源泄漏的实际表现

Python 线程退出后,OS 线程资源由系统回收,但 Python 层的 Thread 对象若没被 gc 掉,会持续持有帧、局部变量引用——尤其当线程函数闭包捕获了大对象时,容易引发内存缓慢上涨。

性能影响:大量短命线程(比如每秒启停上百个)会显著增加 GIL 调度开销和内存碎片;CPython 解释器本身不提供线程池复用,得靠 concurrent.futures.ThreadPoolExecutor

  • 避免在循环里反复 Thread(target=f).start();改用 ThreadPoolExecutor.submit(f) 复用线程
  • 线程函数里别留长生命周期引用:比如把全局 logger数据库连接传进去,不如在函数内按需获取
  • 检查 threading.enumerate() 可发现意外存活的线程,但注意它返回的是当前活跃线程快照,非实时全量视图

真正难调试的,是那些没显式 join、又没设 daemon 的线程——它们卡在 I/O 或死锁里,让主程序无法退出,还查不到堆栈。

text=ZqhQzanResources