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

Python 的 threading.Thread 不是操作系统线程的直接封装
它底层确实调用 pthread_create(Linux/macOS)或 CreateThread(Windows),但中间隔着 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?得用
multiprocessing或asyncio+ 非阻塞 I/O,而不是堆更多threading.Thread - 注意:
threading.Thread.daemon=True的线程会在主线程退出时被强制终止,不会等待其自然结束——这常导致日志没刷盘、临时文件没清理
start() 和 run() 的区别不是语义问题,是执行时机问题
start() 是唯一合法的线程启动入口;它负责注册线程、触发 OS 创建、调度进 GIL 等待队列。run() 只是一个普通方法,直接调用等价于同步执行——根本没开新线程。
常见错误现象:写 t.run() 代替 t.start(),结果所有逻辑串行跑完,还误以为“多线程跑完了”。调试时加 print(threading.current_thread().name) 就能立刻暴露问题。
立即学习“Python免费学习笔记(深入)”;
原本这个程序只是本人两年前初学时练手的,最近拿出来进行了修改,所以叫XmxCms 企业网站管理系统2.0 开发环境:WinXP + VS2008 + SQLServer 2008 + Access开发语言:C#本程序采用 三层架构 + 抽象工厂设计模式 + Linq 实现,目前只做了Access 和 SQL Server ,默认数据库为Access,要更换数据库只需修改web.config 即可
- 永远只调
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替代list,threading.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 或死锁里,让主程序无法退出,还查不到堆栈。







