一、多线程基础:进程与线程的区别、Python中的GIL锁、threading模块入门
各位同学好,我是你们的老朋友。今天咱们聊聊多线程的入门知识。
说实话,我刚开始接触并发编程时,也被进程和线程搞晕过。后来在项目中踩了不少坑,才慢慢理清了头绪。今天我把这些经验分享给你。
1.1 进程 vs 线程:到底差在哪?
先问个问题:你打开浏览器、听音乐、写代码,这些程序是怎么同时运行的?
答案是操作系统在调度。每个运行的程序就是一个进程。而进程内部,还可以拆成更小的执行单元——线程。
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 大(独立内存空间) | 小(共享进程内存) |
| 通信方式 | IPC(管道、队列等) | 直接读写共享变量 |
| 创建速度 | 慢 | 快 |
| 稳定性 | 一个进程挂了不影响其他 | 一个线程崩了可能拖垮整个进程 |
我个人的习惯是:IO密集型任务用多线程,CPU密集型任务用多进程。为什么?后面讲GIL时你就明白了。
核心区别一句话:进程是资源分配的最小单位,线程是CPU调度的最小单位。
1.2 Python的GIL锁:绕不开的坎
很多新手学Python多线程时,都会遇到一个奇怪的现象:开了8个线程,CPU占用率还是只有100%?
嗯,这就是GIL在作祟。
GIL(Global Interpreter Lock),全局解释器锁。它规定:同一时刻,只有一个线程能执行Python字节码。
为什么会这样?
说白了,Python的内存管理不是线程安全的。CPython解释器干脆加了一把大锁,谁拿到锁谁干活。其他线程?等着吧。
注意:GIL只影响CPU密集型任务。如果你的程序在等网络、等磁盘、等数据库,线程会主动释放GIL。所以多线程对IO密集型任务依然有效。
我曾经在一个爬虫项目里,用8个线程抓数据,速度提升了将近6倍。但换成纯计算任务,8个线程和1个线程几乎没区别。这就是GIL的威力。
1.3 threading模块入门:写个多线程程序
Python的threading模块,是标准库自带的。用起来很简单。
先看一个基础例子:
import threading
import time
def worker(name):
print(f"线程 {name} 开始工作")
time.sleep(2)
print(f"线程 {name} 结束工作")
# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
# 启动线程
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print("所有线程执行完毕")
运行结果:
线程 A 开始工作
线程 B 开始工作
(等待2秒)
线程 A 结束工作
线程 B 结束工作
所有线程执行完毕
看到了吗?两个线程几乎是同时启动的。这就是并发执行的效果。
小技巧:用threading.current_thread().name可以获取当前线程的名字,调试时很有用。
1.4 线程生命周期:从创建到销毁
一个线程会经历这几个阶段:
- 新建(New):创建Thread对象,还没启动
- 就绪(Runnable):调用start(),等待CPU调度
- 运行(Running):获得CPU时间片,执行代码
- 阻塞(Blocked):等待IO、等待锁、调用sleep
- 死亡(Dead):run()执行完毕或抛出异常
我刚开始写多线程时,经常忘记调用join()。结果主线程跑完了,子线程还在后台跑。程序退出时,子线程被强制终止,数据都没保存。嗯,这个坑我踩过。
1.5 守护线程:后台的隐形打工人
有一种特殊的线程叫守护线程(Daemon Thread)。它会在主线程结束时自动退出。
import threading
import time
def background_task():
while True:
print("后台任务运行中...")
time.sleep(1)
t = threading.Thread(target=background_task)
t.daemon = True # 设置为守护线程
t.start()
time.sleep(3)
print("主线程结束,守护线程自动退出")
运行结果:
后台任务运行中...
后台任务运行中...
后台任务运行中...
主线程结束,守护线程自动退出
注意:守护线程不能执行关键任务。比如写文件、提交数据库事务。因为主线程退出时,它可能还没执行完。
1.6 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
1.7 避坑指南
最后,分享几个我踩过的坑:
- 忘记join():主线程跑完了,子线程还在后台跑。程序退出时数据丢失。
- 共享变量不加锁:两个线程同时修改同一个列表,结果数据错乱。后面我们会讲锁的用法。
- 误以为多线程一定能加速:CPU密集型任务,多线程反而可能更慢(GIL上下文切换开销)。
我曾经在一个数据处理项目里,用多线程做数值计算。开了16个线程,结果比单线程还慢。后来换成多进程,速度直接翻了8倍。嗯,这个教训很深刻。
好了,今天的内容就到这里。记住:多线程不是银弹,选对工具比盲目堆线程更重要。