Python 并发编程绕不开 GIL,很多人被它坑过。这篇讲清多线程、多进程怎么选、怎么写,附爬虫和 CPU 密集型两个实战例子。
一、多线程:IO 密集型用 thread
import threading
import time
def download(url):
print(f"开始下载: {url}")
time.sleep(2) # 模拟网络请求(IO等待)
print(f"下载完成: {url}")
# 串行 6 秒
# 多线程并发 2 秒
start = time.time()
threads = []
for i in range(3):
t = threading.Thread(target=download, args=(f"url-{i}",))
t.start()
threads.append(t)
for t in threads:
t.join() # 等所有线程结束
print(f"耗时: {time.time() - start:.1f}秒")
# 输出: 耗时约 2.0 秒
线程池版本(更推荐):
from concurrent.futures import ThreadPoolExecutor
urls = [f"url-{i}" for i in range(10)]
with ThreadPoolExecutor(max_workers=5) as pool:
# 提交任务,返回 Future
futures = [pool.submit(download, url) for url in urls]
for f in futures:
f.result() # 阻塞等结果,异常会在这里抛出
二、GIL 是什么(必考)
GIL = 全局解释器锁
Python 同一时刻只有一个线程在执行 Python 字节码
→ 多线程无法利用多核 CPU
为什么有 GIL?
CPython 内存管理(引用计数)不是线程安全的
加 GIL 最简单,牺牲并行换安全
影响:
多线程对 IO 密集型有效(线程等待IO时释放GIL)
多线程对 CPU 密集型无效(只在一个核上跑)
验证 GIL:
# CPU 密集型任务,多线程反而更慢
def compute(n):
return sum(i * i for i in range(n))
# 串行 vs 多线程,CPU 密集型下多线程没有优势
# 因为 GIL 让线程轮流占用 CPU,还有切换开销
三、多进程:CPU 密集型用 Process
from multiprocessing import Process, cpu_count
import os
def heavy_task(n):
"""CPU 密集型:大量计算"""
result = 0
for i in range(n):
result += i * i
print(f"进程 {os.getpid()} 计算完成: {result}")
return result
if __name__ == "__main__":
processes = []
for i in range(cpu_count()): # 进程数 = CPU核数
p = Process(target=heavy_task, args=(5_000_000,))
p.start()
processes.append(p)
for p in processes:
p.join()
每个进程有独立的解释器和 GIL → 真正利用多核。 但注意进程间不共享内存,传数据用队列。
四、进程池 + 队列传数据
from multiprocessing import Pool, Queue
import multiprocessing
def worker(item):
return item * item
def main():
# 进程池:自动管理多个进程
with Pool(4) as pool:
results = pool.map(worker, range(10))
print(results) # [0, 1, 4, 9, ...]
# 进程间通信用 Queue
q = multiprocessing.Queue()
q.put("数据")
print(q.get())
if __name__ == "__main__":
main()
五、异步 asyncio:更高的 IO 并发
IO 密集还能用协程,单线程也能上万并发:
import asyncio
async def fetch(url):
print(f"请求: {url}")
await asyncio.sleep(1) # 模拟IO,协程挂起不占线程
return f"数据: {url}"
async def main():
tasks = [fetch(f"url-{i}") for i in range(5)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
# 5个请求 1 秒完成(并发),单线程
六、怎么选(核心结论)
| 任务类型 | 方案 | 原因 |
|---|---|---|
| IO 密集型(爬虫/请求/读写) | 多线程 / asyncio | 等待IO时让出CPU,GIL不影响 |
| CPU 密集型(计算/压缩/加密) | 多进程 ProcessPool | 绕开GIL,真正用多核 |
| 简单高并发IO | asyncio 协程 | 单线程上万并发,资源占用最小 |
# 推荐:concurrent.futures 统一接口
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
# IO 密集 → 线程池
with ThreadPoolExecutor(max_workers=10) as pool:
...
# CPU 密集 → 进程池
with ProcessPoolExecutor(max_workers=4) as pool:
...
真实爬虫场景最佳实践:
import requests
from concurrent.futures import ThreadPoolExecutor
def crawl(url):
resp = requests.get(url, timeout=5)
return len(resp.content)
urls = ["https://example.com/" + str(i) for i in range(100)]
with ThreadPoolExecutor(max_workers=20) as pool: # 20个并发请求
sizes = list(pool.map(crawl, urls))
print(f"总下载量: {sum(sizes)} 字节")
# 100个请求从串行100秒 → 并发约5秒
总结
Python 并发选择一句话:IO 密集用线程,CPU 密集用进程,超高并发用协程。
多线程 threading → IO 等待时切线程,适合爬虫
多进程 multiprocessing → 绕开 GIL,适合计算
协程 asyncio → 单线程高并发,最轻量
记住 GIL 的坑(CPU 密集别用多线程),再配合 concurrent.futures 统一接口,并发代码就能写得又快又稳。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!

1245

被折叠的 条评论
为什么被折叠?



