为什么需要多线程?
单线程程序一次只能做一件事。如果你的程序里有网络请求、文件IO、数据库查询这类"等结果"的操作,CPU其实在空转。
多线程的核心目的:让CPU在等待的时候,去干别的事。
先搞懂两个概念
| 概念 | 说明 |
|---|---|
| 进程 | 独立的程序,内存隔离,开销大 |
| 线程 | 进程内部的执行单元,共享内存,开销小 |
| GIL | Python的全局解释器锁,同一时刻只有一个线程在执行Python代码 |
⚠️关键结论:Python多线程适合IO密集型任务,不适合CPU密集型任务。
- ✅ 网络请求、文件读写、数据库查询 → 多线程有用
- ❌ 纯计算、图像处理 → 多线程没用(GIL限制),用多进程
最基础的用法
importthreadingimporttimedeftask(name,delay):print(f"{name}开始")time.sleep(delay)print(f"{name}结束")# 创建线程t1=threading.Thread(target=task,args=("任务A",2))t2=threading.Thread(target=task,args=("任务B",1))# 启动线程t1.start()t2.start()# 等待线程结束t1.join()t2.join()print("全部完成")输出:
任务A 开始 任务B 开始 任务B 结束 任务A 结束 全部完成两个任务同时在跑,总耗时约2秒,而不是3秒。
实战:并发请求接口
这是最常见的使用场景:
importthreadingimportrequests urls=["https://api.example.com/1","https://api.example.com/2","https://api.example.com/3",]results=[]lock=threading.Lock()# 保护共享数据deffetch(url):resp=requests.get(url,timeout=5)withlock:# 加锁,避免多个线程同时写列表results.append(resp.status_code)threads=[]forurlinurls:t=threading.Thread(target=fetch,args=(url,))t.start()threads.append(t)fortinthreads:t.join()print(f"完成{len(results)}个请求")进阶:用 ThreadPoolExecutor(推荐)
手动管理线程太麻烦,直接用线程池:
fromconcurrent.futuresimportThreadPoolExecutorimportrequests urls=["url1","url2","url3",...]deffetch(url):returnrequests.get(url).status_codewithThreadPoolExecutor(max_workers=5)asexecutor:results=list(executor.map(fetch,urls))print(results)✅ 自动管理线程数量,不用手动
start/join
✅max_workers控制并发数,避免把对方服务器打挂
三个常见坑
| 坑 | 说明 | 解决方案 |
|---|---|---|
| GIL导致多线程不加速计算 | for i in range(10000000): pass开10个线程也不会变快 | CPU密集型用multiprocessing |
| 共享数据竞争 | 多个线程同时改同一个变量,结果不可预期 | 用threading.Lock()加锁 |
| 线程数开太多 | 每个线程有开销,开1000个线程反而更慢 | IO密集型:线程数 = CPU核数 × 2~5 |
什么时候用多线程?一张表说清
| 场景 | 推荐方案 |
|---|---|
| 请求多个API | ✅ThreadPoolExecutor |
| 读写多个文件 | ✅ 多线程 |
| 爬虫 | ✅ 多线程 |
| 大量数学计算 | ❌ 用multiprocessing |
| 简单脚本,任务少 | ❌ 没必要,单线程就够 |
一句话总结
Python多线程 = 让IO等待的时间不被浪费。记住GIL的存在,别用它做计算,它就很好用。