各位好,今天咱们来聊聊Python里的两个重量级选手:multiprocessing
(多进程)和threading
(多线程)。这俩哥们儿都能让你的程序“同时”干活,但擅长的领域却大不相同。选错了,轻则效率打折,重则适得其反。所以,咱们得摸清它们的脾气,才能让它们各司其职,把活儿干得漂漂亮亮的。
开场白:别被“并发”忽悠了
先来个小概念澄清一下。很多人把“并发”(concurrency)和“并行”(parallelism)混为一谈,以为都是“一起干活”。其实不然。
- 并发: 就像一个人同时处理多项任务。你一边回微信,一边听歌,一边还要想着晚上吃啥。表面上看起来你在同时干三件事,但实际上你的大脑是在快速切换,轮流处理。
- 并行: 就像几个人同时干活。你、你媳妇儿、你老妈,三个人同时包饺子,那就是并行。
threading
实现的是并发,而 multiprocessing
才能实现真正的并行(在多核 CPU 的情况下)。记住这一点,是咱们后续讨论的基础。
第一回合:threading
,I/O 密集型任务的救星
threading
是Python自带的模块,用来创建线程。线程是轻量级的执行单元,多个线程共享同一个进程的内存空间。这就好比一个公司里,大家都在同一个办公室里工作,共享打印机、会议室等资源。
threading
的优势在于:
- 轻量级: 创建和销毁线程的开销比进程小得多。
- 共享内存: 线程之间可以方便地共享数据。
- 适用于 I/O 密集型任务: 当程序需要频繁等待 I/O 操作(比如网络请求、文件读写)时,线程可以释放 CPU,让其他线程继续执行,从而提高整体效率。
但threading
也有它的局限性。由于Python的全局解释器锁(GIL)的存在,同一时刻只能有一个线程真正地执行Python字节码。这就好比,虽然公司里有很多员工,但只有一个打印机可以用,大家得排队,谁先拿到谁用。
所以,对于 CPU 密集型任务,threading
几乎不起作用。因为大家都在争抢 GIL,反而增加了线程切换的开销。
代码示例:用threading
处理网络请求
假设我们要从多个网站下载网页内容。这是一个典型的 I/O 密集型任务。
import threading
import requests
import time
def download_page(url):
"""下载网页内容"""
print(f"开始下载: {url}")
try:
response = requests.get(url)
response.raise_for_status() # 检查请求是否成功
print(f"成功下载: {url}, 长度: {len(response.content)}")
except requests.exceptions.RequestException as e:
print(f"下载失败: {url}, 错误: {e}")
def main():
urls = [
"https://www.example.com",
"https://www.baidu.com",
"https://www.python.org",
"https://www.google.com" # 模拟一些需要等待的请求
]
threads = []
start_time = time.time()
for url in urls:
thread = threading.Thread(target=download_page, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join() # 等待所有线程完成
end_time = time.time()
print(f"总耗时: {end_time - start_time:.2f} 秒")
if __name__ == "__main__":
main()
在这个例子中,我们创建了多个线程,每个线程负责下载一个网页。由于下载网页需要等待网络响应,线程可以释放 CPU,让其他线程继续执行。这样,多个网页可以“同时”下载,大大提高了效率。
第二回合:multiprocessing
,CPU 密集型任务的王者
multiprocessing
是 Python 的另一个模块,用来创建进程。进程是操作系统分配资源的最小单位,每个进程都有自己独立的内存空间。这就好比多个公司,每个公司都有自己的办公室、打印机、会议室等资源。
multiprocessing
的优势在于:
- 绕过 GIL: 每个进程都有自己的 Python 解释器,不受 GIL 的限制。因此,对于 CPU 密集型任务,
multiprocessing
可以充分利用多核 CPU 的性能。 - 隔离性好: 进程之间互不干扰,一个进程崩溃不会影响其他进程。
- 适用于 CPU 密集型任务: 当程序需要进行大量的计算时,
multiprocessing
可以将任务分配到多个进程中并行执行,从而提高效率。
但multiprocessing
也有它的缺点:
- 开销大: 创建和销毁进程的开销比线程大得多。
- 内存占用高: 每个进程都有自己的内存空间,会占用更多的内存。
- 进程间通信复杂: 进程之间不能直接共享数据,需要使用特殊的机制(比如管道、队列、共享内存)进行通信。
代码示例:用multiprocessing
计算圆周率
假设我们要计算圆周率 π 的值。这是一个典型的 CPU 密集型任务。
import multiprocessing
import time
def calculate_pi(num_steps):
"""使用蒙特卡洛方法计算圆周率"""
inside_circle = 0
for _ in range(num_steps):
import random
x = random.random()
y = random.random()
if x*x + y*y <= 1:
inside_circle += 1
pi = 4 * inside_circle / num_steps
return pi
def main():
num_processes = multiprocessing.cpu_count() # 获取 CPU 核心数
num_steps_per_process = 10000000 // num_processes # 每个进程的计算量
pool = multiprocessing.Pool(processes=num_processes) # 创建进程池
start_time = time.time()
results = pool.map(calculate_pi, [num_steps_per_process] * num_processes) # 并行计算
pool.close()
pool.join()
end_time = time.time()
pi = sum(results) / num_processes # 平均多个进程的计算结果,提升准确性
print(f"圆周率: {pi}")
print(f"进程数: {num_processes}")
print(f"总耗时: {end_time - start_time:.2f} 秒")
if __name__ == "__main__":
main()
在这个例子中,我们创建了一个进程池,将计算任务分配到多个进程中并行执行。由于计算圆周率需要进行大量的计算,multiprocessing
可以充分利用多核 CPU 的性能,大大提高了效率。
第三回合:总结与选择指南
现在,咱们来总结一下 threading
和 multiprocessing
的优缺点,并给出一些选择指南。
特性 | threading |
multiprocessing |
---|---|---|
并发/并行 | 并发 | 并行 (在多核 CPU 上) |
GIL 影响 | 受 GIL 限制 | 不受 GIL 限制 |
创建开销 | 小 | 大 |
内存占用 | 小 | 大 |
进程间通信 | 简单 (共享内存) | 复杂 (管道、队列、共享内存) |
隔离性 | 差 (一个线程崩溃可能影响整个进程) | 好 (一个进程崩溃不会影响其他进程) |
适用场景 | I/O 密集型任务 (网络请求、文件读写等) | CPU 密集型任务 (大量计算、图像处理等) |
资源利用率 | 在 I/O 密集型任务中 CPU 利用率低 | 在 CPU 密集型任务中 CPU 利用率高 |
选择指南:
- I/O 密集型任务: 优先选择
threading
。线程可以释放 CPU,让其他线程继续执行,提高整体效率。 - CPU 密集型任务: 优先选择
multiprocessing
。进程可以绕过 GIL,充分利用多核 CPU 的性能。 - 混合型任务: 可以结合使用
threading
和multiprocessing
。比如,主进程使用multiprocessing
处理 CPU 密集型任务,子进程使用threading
处理 I/O 密集型任务。
补充说明:
asyncio
: Python 3.4 引入了asyncio
模块,提供了一种更高效的并发编程模型。asyncio
使用协程(coroutine)来实现并发,避免了线程切换的开销。对于 I/O 密集型任务,asyncio
通常比threading
更高效。- 进程池和线程池:
multiprocessing
和threading
都提供了进程池和线程池的机制,可以预先创建好一些进程或线程,避免频繁创建和销毁的开销。 - 进程间通信:
multiprocessing
提供了多种进程间通信的机制,包括管道、队列、共享内存等。选择哪种机制取决于具体的应用场景。
彩蛋:一个错误的例子
很多新手会犯一个错误,就是把 threading
用在 CPU 密集型任务上,以为多线程就能提高效率。让我们来看一个反例:
import threading
import time
def cpu_bound_task(n):
"""模拟一个 CPU 密集型任务"""
while n > 0:
n -= 1
def main():
num_threads = 4 # 假设有 4 个 CPU 核心
n = 100000000 # 计算量
threads = []
start_time = time.time()
for _ in range(num_threads):
thread = threading.Thread(target=cpu_bound_task, args=(n // num_threads,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"总耗时 (threading): {end_time - start_time:.2f} 秒")
start_time = time.time()
cpu_bound_task(n)
end_time = time.time()
print(f"总耗时 (单线程): {end_time - start_time:.2f} 秒")
if __name__ == "__main__":
main()
运行结果会让你大跌眼镜:多线程的效率甚至不如单线程!这就是 GIL 的威力。
总结的总结:没有银弹
threading
和 multiprocessing
都是强大的工具,但没有万能的解决方案。选择哪种方式取决于具体的任务类型和应用场景。重要的是理解它们的原理和优缺点,才能做出正确的选择。
希望今天的讲解对大家有所帮助。下次再见!