1. 为什么需要多线程与多进程?
当你在Python中处理一个需要长时间运行的任务时,比如从网上下载多个大文件,或者对大量数据进行计算,你会发现程序运行得很慢。这时,多线程和多进程就能派上用场了。
想象你是一个餐厅老板。如果只有一个服务员(单线程),他需要依次接待每位顾客、点餐、上菜、结账。而如果有多个服务员(多线程),就能同时服务多桌客人。多进程则像是开了几家分店,每家店都有自己的服务员团队。
在Python中,多线程适合I/O密集型任务(如网络请求、文件读写),因为线程可以在等待I/O时切换;多进程适合CPU密集型任务(如数值计算),因为每个进程有独立的Python解释器和内存空间。
注意:Python有个全局解释器锁(GIL),它限制了同一时间只能有一个线程执行Python字节码。这就是为什么多线程在CPU密集型任务中表现不佳,而多进程可以绕过这个限制。
2. 多线程编程实战
2.1 创建和启动线程
Python标准库中的threading模块提供了线程支持。最简单的使用方式是创建一个Thread对象:
python复制import threading
import time
def download_file(url):
print(f"开始下载 {url}")
time.sleep(2) # 模拟下载耗时
print(f"完成下载 {url}")
# 创建线程
thread1 = threading.Thread(target=download_file, args=("http://example.com/1.zip",))
thread2 = threading.Thread(target=download_file, args=("http://example.com/2.zip",))
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
2.2 线程同步与通信
当多个线程需要共享数据时,可能会遇到竞争条件。比如两个线程同时修改同一个银行账户余额:
python复制from threading import Lock
class BankAccount:
def __init__(self):
self.balance = 100
self.lock = Lock()
def withdraw(self, amount):
with self.lock: # 获取锁
if self.balance >= amount:
time.sleep(0.1) # 模拟处理延迟
self.balance -= amount
return True
return False
常用的同步机制包括:
Lock:基本互斥锁RLock:可重入锁,同一个线程可以多次获取Semaphore:信号量,控制同时访问资源的线程数Event:事件,用于线程间通信
2.3 线程池的使用
频繁创建销毁线程开销很大,使用线程池可以复用线程:
python复制from concurrent.futures import ThreadPoolExecutor
urls = ["url1", "url2", "url3", "url4"]
with ThreadPoolExecutor(max_workers=4) as executor:
executor.map(download_file, urls)
3. 多进程编程深入
3.1 进程创建与管理
multiprocessing模块提供了类似threading的API:
python复制from multiprocessing import Process
def calculate_square(numbers):
for n in numbers:
print(f"平方 {n}: {n*n}")
if __name__ == '__main__':
numbers = [1, 2, 3, 4]
p1 = Process(target=calculate_square, args=(numbers[:2],))
p2 = Process(target=calculate_square, args=(numbers[2:],))
p1.start()
p2.start()
p1.join()
p2.join()
3.2 进程间通信
进程有独立的内存空间,不能直接共享变量。常用通信方式:
- 队列(Queue):
python复制from multiprocessing import Queue
def worker(q):
while True:
item = q.get()
if item is None:
break
print(f"处理 {item}")
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
for i in range(10):
q.put(i)
q.put(None) # 结束信号
p.join()
- 管道(Pipe):
python复制from multiprocessing import Pipe
parent_conn, child_conn = Pipe()
p = Process(target=worker_func, args=(child_conn,))
p.start()
parent_conn.send("Hello from parent")
print(parent_conn.recv())
p.join()
- 共享内存:
python复制from multiprocessing import Value, Array
counter = Value('i', 0) # 'i'表示整数
arr = Array('d', [0.0, 1.1, 2.2]) # 'd'表示双精度浮点数
3.3 进程池
类似于线程池,进程池可以管理多个工作进程:
python复制from multiprocessing import Pool
def cpu_intensive_task(n):
return n * n
if __name__ == '__main__':
with Pool(4) as p:
results = p.map(cpu_intensive_task, range(10))
print(results)
4. 高级应用与性能优化
4.1 选择合适的并发模型
- I/O密集型:网络请求、文件操作 → 多线程
- CPU密集型:数学计算、图像处理 → 多进程
- 混合型:可以考虑多进程+多线程组合
4.2 避免常见陷阱
-
死锁:多个线程/进程互相等待对方释放锁
- 解决方案:按固定顺序获取锁,设置超时
-
资源竞争:多个线程同时修改共享数据
- 解决方案:使用锁或使用线程安全的数据结构
-
GIL限制:Python的全局解释器锁影响多线程性能
- 解决方案:CPU密集型任务使用多进程,或使用C扩展
4.3 性能调优技巧
-
调整工作线程/进程数:
- I/O密集型:可以设置较多线程(如CPU核心数的2-3倍)
- CPU密集型:通常设置为CPU核心数
-
批量处理:减少线程/进程间通信次数
python复制# 不好:频繁通信 for item in items: q.put(item) # 好:批量处理 batch_size = 100 for i in range(0, len(items), batch_size): q.put(items[i:i+batch_size]) -
使用更高效的并发库:
asyncio:适合高并发的I/O操作concurrent.futures:高层抽象,统一线程/进程接口joblib:科学计算领域的并行工具
4.4 实际案例:并行下载器
python复制import requests
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import urlparse
import os
def download_file(url, save_dir="downloads"):
os.makedirs(save_dir, exist_ok=True)
filename = os.path.basename(urlparse(url).path)
save_path = os.path.join(save_dir, filename)
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(save_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return save_path
urls = [
"http://example.com/file1.zip",
"http://example.com/file2.zip",
"http://example.com/file3.zip"
]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(download_file, urls))
print(f"下载完成:{results}")
5. 调试与监控
5.1 线程/进程状态检查
python复制import threading
import time
def worker():
time.sleep(2)
t = threading.Thread(target=worker)
t.start()
print(f"线程存活: {t.is_alive()}")
print(f"线程名称: {t.name}")
print(f"线程ID: {t.ident}")
5.2 性能分析
使用cProfile分析并发程序性能:
python复制import cProfile
import io
import pstats
from concurrent.futures import ThreadPoolExecutor
def task(n):
return sum(i*i for i in range(n))
def main():
with ThreadPoolExecutor() as executor:
results = list(executor.map(task, [1000000]*10))
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
s = io.StringIO()
ps = pstats.Stats(profiler, stream=s).sort_stats('cumulative')
ps.print_stats()
print(s.getvalue())
5.3 可视化工具
- htop(Linux/macOS):实时查看系统进程和线程
- 任务管理器(Windows):监控CPU和内存使用
- py-spy:Python程序的采样分析器
bash复制
pip install py-spy py-spy top --pid <PID>
6. 替代方案与未来趋势
6.1 asyncio异步编程
对于I/O密集型应用,asyncio提供了更高效的解决方案:
python复制import asyncio
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["http://example.com"]*10
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"获取了 {len(results)} 个页面")
asyncio.run(main())
6.2 分布式任务队列
对于大规模并行处理,可以考虑:
- Celery:分布式任务队列
- Dask:并行计算框架
- Ray:分布式计算框架
6.3 最佳实践总结
- 从小规模开始:先实现单线程/单进程版本,确保正确性
- 逐步引入并发:先尝试多线程,必要时再考虑多进程
- 合理设置并发度:太多线程/进程会导致上下文切换开销
- 重视错误处理:并发环境下的异常更难调试
- 考虑可扩展性:设计时考虑未来可能的扩展需求
我在实际项目中发现,很多性能问题其实不是并发度不够,而是算法或I/O操作本身效率低下。在考虑并发之前,应该先优化单线程性能。比如,一个O(n²)的算法,即使用100个核心并行,也不如优化为O(n log n)的单线程版本。
