Python 从入门到精通
第 33 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择正在阅读 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
36
综合项目:从需求到上线
上一篇学了异步编程,处理”等得多”的任务。但还有一类任务它管不了:CPU 密集——大量计算、图像处理、数据转换,这类任务需要多个 CPU 核心同时干活。这一篇讲线程和进程,以及怎么选。
三个概念先分清
- 进程(process):一个正在运行的程序,有自己的内存空间。多进程 = 多个程序并行,能用到多核 CPU
- 线程(thread):进程里的执行单元,共享进程内存。多线程 = 一个程序里多条”流水线”
- GIL(全局解释器锁):CPython 的机制,同一时刻只有一个线程在执行 Python 代码。这是理解并发选型的钥匙
多线程:threading
先看线程怎么用(最简方式,用 ThreadPoolExecutor 线程池):
from concurrent.futures import ThreadPoolExecutor
import time
def work(name, seconds):
time.sleep(seconds) # 模拟 IO 等待
return f"{name} 完成"
start = time.time()
with ThreadPoolExecutor(max_workers=3) as pool:
results = list(pool.map(work, ["任务A", "任务B", "任务C"], [1, 2, 3]))
print(results) # ['任务A 完成', '任务B 完成', '任务C 完成']
print(f"耗时 {time.time() - start:.2f} 秒") # ≈3 秒(并发执行)
ThreadPoolExecutor 是线程池:max_workers 控制同时几个线程,pool.map(函数, 参数列表) 批量派活。比手写 threading.Thread 简洁得多。
GIL:为什么线程算不快
关键实验:多线程跑 纯计算 任务:
from concurrent.futures import ThreadPoolExecutor
import time
def heavy_calc(n):
total = 0
for i in range(n):
total += i * i
return total
start = time.time()
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(heavy_calc, [5_000_000] * 4))
print(f"4 线程纯计算耗时:{time.time() - start:.2f} 秒")
# 对比:单线程
start = time.time()
for _ in range(4):
heavy_calc(5_000_000)
print(f"单线程耗时:{time.time() - start:.2f} 秒")
结果会让你意外:4 线程 ≈ 单线程,甚至更慢(线程切换有开销)。原因就是 GIL——Python 解释器同一时刻只让一个线程跑字节码,多线程抢一个锁,纯计算根本没法并行。
但 IO 等待时线程会释放 GIL(sleep、网络、磁盘等待都不占锁),所以上一篇那种”等网络”的任务,多线程也能提速。结论:
- IO 密集:线程/异步都能提速(等的时候让出 GIL)
- CPU 密集:线程没用,得上多进程(每个进程有自己的 GIL)
多进程:multiprocessing
多进程让每个进程独立跑 Python,绕过 GIL,真正用上多核:
from concurrent.futures import ProcessPoolExecutor
import time
def heavy_calc(n):
total = 0
for i in range(n):
total += i * i
return total
start = time.time()
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(heavy_calc, [5_000_000] * 4))
print(f"4 进程耗时:{time.time() - start:.2f} 秒") # 明显快于单线程(多核并行)
用法和线程池几乎一样,把 ThreadPoolExecutor 换成 ProcessPoolExecutor 即可。
注意事项:
- 进程间不共享内存,参数和返回值会被序列化传递(pickle)——传大对象很慢
- Windows 上必须把入口代码放
if __name__ == "__main__":里,否则递归报错
进程间通信:Queue
进程不共享变量,要传递结果用队列:
import multiprocessing
def producer(q, name):
for i in range(3):
q.put(f"{name}-{i}") # 放数据
def consumer(q):
while not q.empty():
print("收到:", q.get()) # 取数据
if __name__ == "__main__":
q = multiprocessing.Queue()
p1 = multiprocessing.Process(target=producer, args=(q, "A"))
p2 = multiprocessing.Process(target=consumer, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
日常用 ProcessPoolExecutor 就够,手写 Process + Queue 了解即可。
共享变量的坑:竞态条件
多线程共享内存,多个线程同时改一个变量会出乱子:
import threading
counter = 0
def increment():
global counter
for _ in range(1_000_000):
counter += 1 # 不是原子操作!读-改-写三步,线程间会互相打断
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 应该是 5_000_000,实际经常少一些(数据竞争)
解决:用锁(Lock)保护共享区域:
import threading
counter = 0
lock = threading.Lock() # 锁
def increment():
global counter
for _ in range(1_000_000):
with lock: # 加锁:同时只有一个线程能进
counter += 1
# ... 同上启动线程 ...
print(counter) # 5000000 正确了
经验:多线程里尽量别共享可变状态;必须共享就加锁;能避免就避免(用线程池的返回值代替共享变量)。
并发选型总表
| 场景 | 工具 | 原因 |
|---|---|---|
| IO 密集(网络/文件/数据库等待) | asyncio 或 ThreadPoolExecutor | 等待时让出 GIL,单线程扛大量并发 |
| CPU 密集(计算/图像/数据处理) | ProcessPoolExecutor / multiprocessing | 多进程绕过 GIL,用满多核 |
| 两者混合 | 异步 + to_thread / 进程池 | 各取所长 |
一句话:等得多用异步/线程,算得多用多进程。
新手坑
坑 1:CPU 任务用线程,发现没提速。GIL 卡着,换 ProcessPoolExecutor。
坑 2:多线程共享变量出诡异结果。竞态条件,加锁或改用返回值传递。
坑 3:进程池传超大对象。序列化开销比计算还大,慢得离谱。大数据用共享内存或文件。
小结
- 进程有独立内存、能用多核;线程共享内存、受 GIL 限制
- GIL:同一时刻只有一个线程执行 Python 代码;IO 等待释放 GIL,纯计算不释放
- IO 密集 → 异步/线程;CPU 密集 → 多进程(ProcessPoolExecutor)
- 共享变量要加锁(with lock:),最好别共享
- 并发选型一句话:等得多用异步/线程,算得多用多进程
练习
- 用 ThreadPoolExecutor 并发下载 5 个文件(模拟 sleep),和串行对比耗时
- 用 ProcessPoolExecutor 并行计算 4 个大数的质数判断,对比单线程耗时
- 思考:为什么 GIL 存在的情况下,多线程处理网络请求依然能提速?”等待”和”计算”在 GIL 面前有什么区别?