前言在写第一行多线程代码之前先搞清楚「线程」到底是什么比记住Thread的用法更重要。很多诡异的 bug——数据被莫名覆盖、程序退不出来、日志顺序乱成一团——根源都是对线程模型的理解有偏差而不是语法不会写。常见的误解有两个。一是把线程当成「更轻的函数调用」以为start()之后就一定按顺序执行二是把线程当成「能提速的黑盒」遇到慢就想开线程。前者忽略了线程之间的执行顺序完全不确定后者忽略了CPython 有 GIL多线程对计算密集任务是帮不上忙的。本文只讲概念与模型线程和进程的区别、线程的状态、主线程与守护线程、以及为什么必须有同步机制。代码示例以 CPython 3.8 及以上为基准只做演示不涉及具体业务。一、线程是什么从「执行流」说起一个正在运行的程序至少有一条执行流从入口开始一条条语句往下走。这条执行流就是主线程main thread。线程thread就是一条独立的执行流。给程序加一个线程等于让它同时走两条路一条路在等待网络返回另一条路可以继续读文件。操作系统负责在它们之间来回切换让每条路都感觉自己一直在跑。线程属于进程process。一个进程可以有很多线程它们共享这个进程的内存空间所以能直接读同一个对象打开的文件和套接字描述符环境变量、当前工作目录它们各自独占的只有调用栈函数调用到哪一层了寄存器状态当前算到哪了「共享内存」是线程最核心的特征它既带来了线程间通信的方便也带来了所有同步问题。对比项多进程多线程内存各自独立同一进程内共享数据共享方式需要序列化或共享内存直接读写同一个对象隔离性强一个崩了不牵连弱一个线程出致命错误可能带走整个进程创建开销大小CPython 能否真并行能默认不能受 GIL 限制二、并发不等于并行这两个词经常被混用但含义不同并发concurrency多件事交替推进。单核 CPU 也能做到并发靠快速切换。并行parallelism多件事同时推进需要多个物理核心。多线程在单核上就能实现并发只有在多核上、且不被 GIL 挡住时才谈得上并行。所以「用了线程」和「跑得更快」之间没有必然联系——如果任务是纯计算线程只是在几个核心中的一个上交替跑总时间不降反升。三、线程的四种状态理解状态有助于看懂线程为什么「不听话」状态含义新建new对象已创建还没start()就绪 / 运行runnable已启动等待调度或正在跑阻塞blocked在等锁、等 I/O、在sleep结束terminatedrun()正常返回或抛出未捕获异常Python 通过is_alive()暴露「是否还活着」这一个问题它把「就绪」和「阻塞」都算作 alive把「新建」和「结束」都算作 not alive。看下面这段代码# 适用于 Python 3.8import threadingdef work():passt threading.Thread(targetwork)print(t.is_alive()) # False还没 start()t.start()t.join()print(t.is_alive()) # False已经结束判断线程是否活着要配合join(timeout)使用join()永远返回None想知道它到底是「结束了」还是「超时了」就在join(timeout)之后再看一次is_alive()。四、主线程与守护线程主线程是程序启动时的那条执行流它不是守护线程。程序什么时候退出官方文档给的规则很明确默认情况下Python 程序会等到所有非守护线程都结束后才退出一旦只剩守护线程daemon thread整个程序就退出。守护线程常用于「跟着主程序一起生、一起死」的辅助任务比如后台心跳。但文档特别提醒守护线程在解释器关闭时是被突然掐断的它持有的文件句柄、数据库事务等可能来不及正确释放。所以要写关键数据请不要设成守护线程而是设成非守护再用一个信号例如threading.Event通知它主动收尾。# 适用于 Python 3.8import threadingimport timestop_event threading.Event()def background():# 用 Event 做协作式退出而不是等着被强杀while not stop_event.is_set():time.sleep(0.05)print(后台线程已优雅退出)t threading.Thread(targetbackground, daemonTrue)t.start()time.sleep(0.2)stop_event.set() # 通知它停t.join(timeout1) # 给它一点时间收尾print(主线程结束)daemon属性必须在start()之前设置start()之后再改会抛RuntimeError。旧的setDaemon()/isDaemon()写法自 Python 3.10 起已废弃直接用daemon属性即可。五、为什么必须同步共享内存的代价线程共享内存意味着两个线程可能同时操作同一个对象。由于解释器会在字节码之间切换线程切换间隔由sys.getswitchinterval()控制一段「看起来连续」的代码可能被打断。最经典的例子是累加# 适用于 Python 3.8import threadingtotal 0def add():global totalfor _ in range(50_000):total 1ts [threading.Thread(targetadd) for _ in range(3)]for t in ts:t.start()for t in ts:t.join()print(total) # 期望 150000实际通常偏小total 1不是原子操作它要先读total再加一再写回。两个线程如果都读到了同一个旧值就会有一个人的加一被覆盖这就是竞态条件race condition——程序不会报错只是结果悄悄错了。这类 bug 的危险之处就在于此它不一定每次复现测试环境跑得很正常上了线偶发。解决办法是给临界区加锁# 适用于 Python 3.8import threadingtotal 0lock threading.Lock()def add():global totalfor _ in range(50_000):with lock:total 1with lock:在进出时自动acquire()/release()即使临界区里抛异常也会释放比手写try/finally更不容易漏。六、一个可以观察现象的小例子下面这段代码用不同的切换间隔跑同一个多线程程序可以直观感受「线程调度是由解释器和操作系统共同决定的不保证顺序」。# 适用于 Python 3.8import sysimport threadingdef show(tag):for i in range(3):print(f{tag}-{i}, end )sys.setswitchinterval(0.000001) # 切换得非常频繁ts [threading.Thread(targetshow, args(tag,)) for tag in (A, B)]for t in ts:t.start()for t in ts:t.join()print() # 换行多跑几次会发现输出顺序并不稳定可能是A-0 B-0 A-1 B-1 ...也可能出现别的交错。这不是 bug而是并发的本质。永远不要依赖多线程的输出顺序也不要用「打印顺序对上了」来证明线程安全。注意sys.setswitchinterval()是全局设置会影响整个解释器里所有线程的调度行为生产代码里不要随手改。常见坑点1. 把线程当成顺序执行的函数调用❌t.start()之后立刻假设线程里的赋值已经完成。 ✅start()只是「已启动」结果是否就绪要用join()或Event来确认。2. 用守护线程做关键的写盘/写库❌Thread(targetsave, daemonTrue)主程序一结束就丢数据。 ✅ 关键任务设非守护并join()确需后台运行就用Event协作退出。3.start()之后再改daemon❌t.start(); t.daemon True—— 抛RuntimeError。 ✅ 在构造时写Thread(targetf, daemonTrue)。4. 以为「没报错就是对的」❌ 多线程累加看到结果大致对就放过。 ✅ 竞态是概率事件共享可变状态一律加锁或用queue.Queue。5. 依赖多线程的输出顺序❌ 用print的先后顺序判断哪个线程先跑。 ✅ 顺序不确定是正常现象需要次序就显式用锁或队列。6. 用is_alive()代替join()❌ 轮询while t.is_alive(): pass忙等把 CPU 占满。 ✅ 用t.join()阻塞等待或用join(timeout)设上限。7. 忘了join()也忘了with块❌ 手工Thread(target...)一批又不等待主线程提前退出。 ✅ 统一join()或换成ThreadPoolExecutor的with块自动收尾。8. 以为线程能加速纯计算❌ 用线程跑大量数学运算期待多核加速。 ✅ 受 GIL 限制CPU 密集任务请用multiprocessing/ProcessPoolExecutor。总结概念一句话结论线程进程内的一条独立执行流共享内存、独享调用栈并发 vs 并行并发是交替推进并行要多个核且不受 GIL 限制线程状态新建 → 就绪/运行 → 阻塞 → 结束is_alive()只回答「活着吗」主线程非守护程序会等所有非守护线程结束守护线程退出时被突然掐断不适合做关键写操作同步共享可变状态必须加锁或改用queue.QueueGILCPython 同一时刻只跑一个线程的字节码3.13 起有实验性 no-GIL 构建认识线程的关键是接受三件事执行顺序不确定、共享内存需要保护、受 GIL 限制的多线程只对 I/O 有效。把这三条内化之后后面学Lock、Condition、线程池都会顺理成章。