想象一下:你需要处理100个网络请求,每个请求要等待1秒才能返回结果。如果按顺序一个一个处理,总共需要100秒;但如果能同时处理这些请求,可能只需要1秒多一点——这就是并发与并行的魔力。
在Python中,合理利用并发与并行技术,能让程序在处理多个任务时效率呈几何级提升。本文将揭秘三种并发并行模式的实战技巧,附代码案例和性能对比,帮你选对"加速模式"。
一、多线程:I/O密集型任务的"流水线"
I/O密集型任务(如网络请求、文件读写、数据库操作)的特点是:大部分时间在等待(等待数据传输、等待磁盘响应),真正占用CPU的时间很少。这时候,多线程就像工厂的流水线——一个工位在等待材料时,其他工位可以继续工作。
为什么多线程适合I/O密集型任务?
Python的GIL(全局解释器锁)会限制多线程同时执行Python字节码,但在等待I/O时,线程会释放GIL,让其他线程有机会运行。这就像一群人轮流使用一台咖啡机:有人在等咖啡时,其他人可以先操作机器。
实战案例:多线程下载图片(网络I/O任务)
场景:从网络下载10张图片,比较串行和多线程的效率差异。
1. 串行下载(单线程)
import time
import requests
# 图片URL列表(示例)
IMAGE_URLS = [
"https://picsum.photos/400/300?random=1",
"https://picsum.photos/400/300?random=2",
# ... 共10个URL
] * 10 # 总共10个图片地址
def download_image(url, idx):
"""下载单张图片"""
response = requests.get(url)
# 模拟保存文件
with open(f"image_{idx}.jpg", "wb") as f:
f.write(response.content)
return f"image_{idx}下载完成"
# 串行执行
start = time.time()
for i, url in enumerate(IMAGE_URLS):
download_image(url, i)
serial_time = time.time() - start
print(f"串行下载耗时:{serial_time:.2f}秒") # 约10秒(每个请求约1秒)
2. 多线程下载(用concurrent.futures
)
import time
import requests
from concurrent.futures import ThreadPoolExecutor
# 同上面的IMAGE_URLS和download_image函数
# 多线程执行
start = time.time()
# 创建线程池(最多5个线程)
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交所有任务
futures = [executor.submit(download_image, url, i)
for i, url in enumerate(IMAGE_URLS)]
# 获取结果
for future in futures:
future.result()
thread_time = time.time() - start
print(f"多线程下载耗时:{thread_time:.2f}秒") # 约2秒(提速5倍)
3. 多线程的"最佳实践"
- 线程数不是越多越好:过多线程会导致线程切换开销增大,通常设置为
CPU核心数 * 5
左右 - 用
ThreadPoolExecutor
比直接操作threading.Thread
更简洁,自动管理线程生命周期 - 避免在多线程中使用全局变量,如需共享数据,用
threading.Lock
保护临界区
# 带锁的共享变量示例
from threading import Lock
counter = 0
lock = Lock() # 创建锁
def increment():
global counter
with lock: # 自动获取和释放锁
counter += 1
二、多进程:CPU密集型任务的"分身术"
CPU密集型任务(如数学计算、数据处理、图像处理)的特点是:需要持续占用CPU进行计算,几乎不需要等待。这时候,多线程会因为GIL的限制而无法真正并行,而多进程能通过"分身术"——创建多个Python解释器进程,充分利用多核CPU。
为什么多进程适合CPU密集型任务?
每个进程都有独立的Python解释器和内存空间,不受GIL限制。就像多台咖啡机同时工作,而不是一群人抢一台咖啡机。
实战案例:多进程计算质数(CPU密集型任务)
场景:计算100万以内的质数,比较单进程和多进程的效率。
1. 单进程计算
import time
def is_prime(n):
"""判断一个数是否为质数"""
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
def count_primes_range(start, end):
"""计算指定范围内的质数数量"""
count = 0
for num in range(start, end):
if is_prime(num):
count += 1
return count
# 单进程计算1-100万的质数
start = time.time()
total = count_primes_range(1, 1_000_000)
single_process_time = time.time() - start
print(f"质数总数:{total}")
print(f"单进程耗时:{single_process_time:.2f}秒") # 约10秒
2. 多进程计算(用multiprocessing
)
import time
from multiprocessing import Pool
# 同上面的is_prime和count_primes_range函数
def count_primes_multiprocess():
# 拆分任务:将1-100万分成4个区间(假设有4核CPU)
ranges = [
(1, 250_000),
(250_000, 500_000),
(500_000, 750_000),
(750_000, 1_000_000)
]
# 创建进程池(4个进程,对应4核CPU)
with Pool(processes=4) as pool:
# 并行执行任务
results = pool.starmap(count_primes_range, ranges)
# 汇总结果
return sum(results)
# 多进程计算
start = time.time()
total = count_primes_multiprocess()
multi_process_time = time.time() - start
print(f"质数总数:{total}")
print(f"多进程耗时:{multi_process_time:.2f}秒") # 约2.5秒(提速4倍,接近CPU核心数)
3. 多进程的"注意事项"
- 进程间通信成本高:尽量减少进程间数据传输,可用
multiprocessing.Queue
或Pipe
- 避免创建过多进程:通常设置为
CPU核心数
或CPU核心数 + 1
- 进程启动开销大:适合长时间运行的任务,不适合短任务(启动时间可能超过任务本身)
- Windows系统中,多进程代码需放在
if __name__ == "__main__":
下
# Windows系统多进程正确写法
if __name__ == "__main__":
start = time.time()
total = count_primes_multiprocess()
print(f"多进程耗时:{time.time() - start:.2f}秒")
三、异步编程:高并发I/O的"高效调度员"
当I/O密集型任务的并发量非常高(如同时处理上千个网络请求),多线程会因为线程创建和切换的开销变得低效。这时候,异步编程就像一位高效的调度员——用单线程(或少量线程)管理成千上万的任务,在等待I/O时切换到其他任务,实现"非阻塞"运行。
异步编程的核心概念
- 协程(Coroutine):可暂停和恢复的函数(用
async def
定义),比线程更轻量 - 事件循环(Event Loop):调度协程的"大脑",负责决定何时运行哪个协程
- await:暂停协程,等待异步操作完成(如网络请求),期间事件循环可运行其他协程
实战案例:异步爬取网页(高并发I/O)
场景:同时爬取100个网页,比较多线程和异步的效率。
1. 异步爬取(用asyncio
和aiohttp
)
import time
import asyncio
import aiohttp # 需要安装:pip install aiohttp
# 100个测试URL
URLS = [f"https://httpbin.org/get?random={i}" for i in range(100)]
async def fetch_url(session, url):
"""异步获取网页内容"""
async with session.get(url) as response:
return await response.text() # 等待响应,不阻塞事件循环
async def async_crawl():
"""异步爬取所有URL"""
# 创建异步HTTP客户端
async with aiohttp.ClientSession() as session:
# 创建所有任务
tasks = [fetch_url(session, url) for url in URLS]
# 并发执行所有任务
await asyncio.gather(*tasks)
# 运行异步程序
start = time.time()
asyncio.run(async_crawl())
async_time = time.time() - start
print(f"异步爬取耗时:{async_time:.2f}秒") # 约0.5秒
2. 对比多线程爬取
import time
import requests
from concurrent.futures import ThreadPoolExecutor
# 同上面的URLS列表
def sync_fetch(url):
"""同步获取网页内容"""
response = requests.get(url)
return response.text
def thread_crawl():
"""多线程爬取"""
with ThreadPoolExecutor(max_workers=10) as executor:
executor.map(sync_fetch, URLS)
start = time.time()
thread_crawl()
thread_time = time.time() - start
print(f"多线程爬取耗时:{thread_time:.2f}秒") # 约2秒(异步快4倍)
3. 异步编程的"适用场景"
- 高并发网络请求(API调用、网页爬取)
- 大量并发的文件I/O(需用
aiofiles
库) - 实时通讯应用(聊天、通知)
# 异步文件读写示例(需安装aiofiles)
import aiofiles
async def async_write_file(filename, content):
async with aiofiles.open(filename, 'w') as f:
await f.write(content) # 非阻塞写入
并发并行方案的"选择指南"
任务类型 | 推荐方案 | 典型场景 | 优势 | 注意事项 |
---|---|---|---|---|
I/O密集型 | 多线程(ThreadPoolExecutor) | 少量文件读写、数据库操作 | 实现简单,线程切换成本低 | 线程数不宜过多(<100) |
I/O密集型(高并发) | 异步编程(asyncio) | 上千个网络请求、实时通讯 | 内存占用低,支持超高并发 | 需使用异步库(aiohttp等) |
CPU密集型 | 多进程(Pool) | 数学计算、数据处理 | 充分利用多核CPU,规避GIL | 进程启动成本高,通信开销大 |
选择口诀:
- 计算多用多进程,I/O多用多线程
- 高并发I/O选异步,轻量高效无阻塞
实战经验:并发优化的"避坑指南"
-
不要过早优化:先确认瓶颈在并发,再选择优化方案
# 用cProfile分析瓶颈 import cProfile cProfile.run("my_task()", sort="cumtime")
-
控制并发量:
- 多线程:一般不超过100个
- 多进程:等于CPU核心数
- 异步:可支持数千个,但受限于系统文件描述符
-
异常处理:
- 多线程/进程:用
try-except
捕获单个任务的异常 - 异步:用
try-except
包裹await
语句
- 多线程/进程:用
-
资源释放:
- 多线程/进程:用
with
语句自动释放池资源 - 异步:用
async with
管理异步上下文(如连接)
- 多线程/进程:用
总结:让Python"一心多用"的艺术
并发与并行不是银弹,但却是处理多任务时的"加速器"。掌握这三种模式:
- 多线程像"流水线",适合普通I/O任务
- 多进程像"分身术",适合CPU密集计算
- 异步编程像"高效调度员",适合高并发I/O
记住:最好的并发方案是最适合当前任务的方案。先用简单的多线程解决问题,当并发量增长时再考虑异步;遇到计算瓶颈时,果断用多进程利用多核CPU。
通过合理运用这些技术,你的Python程序将告别"单打独斗",实现真正的"一心多用",在处理多任务时效率倍增!