🔥 Python 多线程编程从入门到精通:原理+实战+最佳实践
- [一、先搞懂核心:进程 vs 线程 🧩](#一、先搞懂核心:进程 vs 线程 🧩)
-
- [1. 核心定义与关系](#1. 核心定义与关系)
- [2. 性能关键:IO 场景下的优势](#2. 性能关键:IO 场景下的优势)
- [3. 进程与线程关系可视化](#3. 进程与线程关系可视化)
- [二、多线程为什么快?爬虫场景秒懂 ⚡️](#二、多线程为什么快?爬虫场景秒懂 ⚡️)
-
- [1. 串行 vs 多线程 对比](#1. 串行 vs 多线程 对比)
- [2. GIL 与线程切换原理](#2. GIL 与线程切换原理)
- [三、实战第一种:Thread 类直接实例化 ✍️](#三、实战第一种:Thread 类直接实例化 ✍️)
-
- [1. 核心步骤](#1. 核心步骤)
- [2. 完整实战代码](#2. 完整实战代码)
- [3. 关键机制:守护线程 `setDaemon()`](#3. 关键机制:守护线程
setDaemon()) - [4. 关键机制:阻塞等待 `join()`](#4. 关键机制:阻塞等待
join())
- [四、实战第二种:继承 Thread 类 🎯](#四、实战第二种:继承 Thread 类 🎯)
-
- [1. 核心规则](#1. 核心规则)
- [2. 完整实战代码](#2. 完整实战代码)
- [3. 两种写法适用场景对比](#3. 两种写法适用场景对比)
- [五、多线程核心知识点总结 📌](#五、多线程核心知识点总结 📌)
- 六、下期预告
在 python 开发中,并发编程是提升程序效率、优化资源利用率的核心技能,而多线程作为并发编程的基础方案,几乎贯穿所有 IO 密集型场景。从爬虫提速到接口并发处理,多线程都能以轻量、高效的方式,让程序告别低效串行,真正实现"同时干活"。
本文将从零拆解 python 多线程核心原理,手把手实现两种主流编码方式,详解守护线程与 join() 阻塞机制,搭配实战代码与可视化逻辑,帮你彻底吃透多线程!
一、先搞懂核心:进程 vs 线程 🧩
很多初学者会混淆进程 和线程,这是理解多线程的第一步,先理清两者的关系与定位:
1. 核心定义与关系
-
进程:操作系统资源分配的最小单元,独立占有内存、文件句柄等系统资源,体积大、调度成本高。
-
线程:操作系统调度执行的最小单元 ,依附于进程存在,共享进程资源,调度轻量、切换更快。
简单理解:进程是容器,线程是干活的工人,一个容器里可以有多个工人协同工作。
2. 性能关键:IO 场景下的优势
对于IO 密集型程序(网络请求、文件读写、数据库查询):
✅ 多线程 ≈ 多进程 性能
✅ 线程调度更轻量,系统开销远小于进程
✅ 遇到 IO 阻塞时,GIL 自动释放,线程无缝切换
这也是 python 多线程最适合的场景!
3. 进程与线程关系可视化
操作系统
进程1
资源独立
进程2
资源独立
线程1
共享进程资源
线程2
共享进程资源
线程3
共享进程资源
线程1
共享进程资源
图表说明:进程相互独立,线程隶属于进程并共享资源,操作系统直接调度线程执行。
二、多线程为什么快?爬虫场景秒懂 ⚡️
以最经典的爬虫场景举例,瞬间理解多线程的并发价值:
1. 串行 vs 多线程 对比
-
串行执行:先爬列表页 → 等待网络返回 → 再爬详情页 → 等待网络返回,等待时间双倍消耗。
-
多线程执行:线程1爬列表页、线程2爬详情页,IO 阻塞时线程切换,等待时间被充分利用,总耗时≈最长单线程耗时。
2. GIL 与线程切换原理
python 的 GIL(全局解释器锁)不会影响 IO 密集型场景的并发效率:
当线程执行网络请求(Socket IO) 时,GIL 自动释放,另一个线程立即接管执行,实现伪并发,效率远超串行。
三、实战第一种:Thread 类直接实例化 ✍️
这是 python 多线程最简写法,适合逻辑简单、快速实现的场景。
1. 核心步骤
-
导入
threading模块 -
定义线程执行的函数
-
实例化
Thread,指定target(执行函数)和args(参数) -
调用
start()启动线程
2. 完整实战代码
python
import threading
import time
# 模拟爬取详情页
def get_detail_html(url):
print(f"✅ get_detail_html started:{url}")
time.sleep(2) # 模拟网络IO等待
print(f"✅ get_detail_html end:{url}")
# 模拟爬取列表页URL
def get_detail_url(url):
print(f"✅ get_detail_url started:{url}")
time.sleep(4) # 模拟网络IO等待
print(f"✅ get_detail_url end:{url}")
if __name__ == "__main__":
start_time = time.time()
# 实例化线程
t1 = threading.Thread(target=get_detail_html, args=("https://xxx.com/1",))
t2 = threading.Thread(target=get_detail_url, args=("https://xxx.com",))
# 启动线程
t1.start()
t2.start()
# 阻塞主线程,等待子线程执行完毕
t1.join()
t2.join()
print(f"⏱ 总耗时:{time.time() - start_time} s")
3. 关键机制:守护线程 setDaemon()
-
作用:将线程设为守护线程,主线程退出时,守护线程直接被终止,不等待执行完毕。
-
用法:
t.setDaemon(True)(必须在start()前设置)
4. 关键机制:阻塞等待 join()
-
作用:让主线程阻塞等待子线程执行完成,再继续后续逻辑。
-
效果:总耗时 = 最长子线程耗时(示例中为4秒),证明并发执行。
四、实战第二种:继承 Thread 类 🎯
当业务逻辑复杂、需要封装多个方法时,继承 Thread 类是更优雅的方案,符合面向对象设计。
1. 核心规则
-
自定义类继承
threading.Thread -
必须重写 **
run()** 方法(线程执行逻辑) -
不要重写
start()方法 -
可重写
__init__传递自定义参数
2. 完整实战代码
python
import threading
import time
# 继承Thread类,封装详情页爬取
class GetDetailHtml(threading.Thread):
def __init__(self, name, url):
super().__init__(name=name) # 调用父类构造
self.url = url
def run(self):
print(f"✅ {self.name} started:{self.url}")
time.sleep(2)
print(f"✅ {self.name} end:{self.url}")
# 继承Thread类,封装列表页爬取
class GetDetailUrl(threading.Thread):
def __init__(self, name, url):
super().__init__(name=name)
self.url = url
def run(self):
print(f"✅ {self.name} started:{self.url}")
time.sleep(4)
print(f"✅ {self.name} end:{self.url}")
if __name__ == "__main__":
start_time = time.time()
t1 = GetDetailHtml(name="detail_thread", url="https://xxx.com/1")
t2 = GetDetailUrl(name="url_thread", url="https://xxx.com")
t1.start()
t2.start()
t1.join()
t2.join()
print(f"⏱ 总耗时:{time.time() - start_time} s")
3. 两种写法适用场景对比
| 写法 | 优点 | 适用场景 |
|---|---|---|
| 直接实例化 Thread | 代码简洁、快速编写 | 逻辑简单、动态创建线程、线程池 |
| 继承 Thread 类 | 封装性强、可扩展、易维护 | 逻辑复杂、需要自定义属性/方法 |
五、多线程核心知识点总结 📌
-
线程是进程的子集,是 OS 调度最小单元,IO 场景下轻量高效。
-
两种实现方式 :直接实例化 / 继承 Thread 重写
run()。 -
守护线程 :
setDaemon(True)→ 主线程退出,守护线程终止。 -
阻塞等待 :
join()→ 主线程等待子线程完成,保证逻辑完整。 -
IO 密集型优先用多线程,CPU 密集型推荐多进程。
六、下期预告
下一篇将深入讲解线程间通信(Queue、Event、Lock),解决多线程数据安全与协同问题,让并发编程更稳定、更安全!
