Python 中的并发编程:Thread 与 Process (多线程与多进程) 深度解析与实战
在现代软件开发中,为了提升程序的执行效率和响应速度,我们经常需要让程序“同时”执行多个任务。Python 提供了丰富的并发编程支持,其中最基础也最常用的就是多线程 (Threading) 与多进程 (Multiprocessing)。
然而,由于 Python(特指 CPython 解释器)中存在著名的 GIL(全局解释器锁),导致很多初学者在编写并发程序时遇到了“多线程反而变慢”的困惑。
本文将带你深入剖析 Python 中的多线程与多进程机制,分析 GIL 的本质,并通过实战代码演示如何在不同场景下选择正确的并发方案。
一、 核心概念:线程与进程的区别
在深入代码之前,我们必须厘清操作系统层面进程与线程的基本定义与区别:
| 特性 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 定义 | 资源分配的最小单位,是一个运行中程序的实例。 | CPU 调度的最小单位,是进程中的实际运作单位。 |
| 内存空间 | 拥有独立的内存空间,进程间数据不共享。 | 共享所属进程的内存空间和资源。 |
| 创建开销 | 创建和销毁的系统开销较大。 | 创建和销毁开销较小,属于轻量级。 |
| 通信机制 | 进程间通信 (IPC) 复杂,需要 Queue、Pipe 或 Shared Memory。 | 线程间可以直接读写共享变量,但需要注意线程安全(加锁)。 |
| 安全性 | 一个进程崩溃不会影响其他进程,稳定性强。 | 一个线程崩溃可能导致整个进程崩溃。 |
二、 全局解释器锁 (GIL) 及其影响
1. 什么是 GIL?
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中的一个互斥锁。它的主要作用是阻止多个线程在同一时刻执行 Python 字节码。简而言之,哪怕你的电脑拥有 16 个 CPU 核心,在同一个 Python 进程中,同一时间也只有一个线程能在 CPU 上运行。
2. 为什么会有 GIL?
历史原因。在 Python 诞生初期,多核 CPU 尚未普及。为了简化解释器设计,保证 CPython 的内存管理(尤其是基于引用计数的垃圾回收机制)是线程安全的,开发者引入了 GIL。这虽然让单线程性能极大提升,且 C 语言扩展非常容易编写,但也为后来的多核并发留下了隐患。
3. 任务类型与并发选择
由于 GIL 的存在,我们将任务分为两类进行讨论: * I/O 密集型任务 (I/O-Bound):如网络请求(爬虫)、文件读写、数据库交互。在进行 I/O 操作时,CPU 处于闲置等待状态。Python 线程会在等待 I/O 时主动释放 GIL,因此多线程在 I/O 密集型任务中非常有效。 * CPU 密集型任务 (CPU-Bound):如数值计算、图像处理、加密解密。此类任务需要持续消耗 CPU 算力。由于 GIL 的限制,多线程不仅无法利用多核,反而会因为频繁的线程切换上下文导致效率下降。因此对于 CPU 密集型任务,必须使用多进程。
三、 多线程实战:Threading 与线程池
在 Python 中,我们可以使用 threading 模块或更高层的 concurrent.futures.ThreadPoolExecutor。推荐使用后者,因为它提供了开箱即用的线程池管理。
实战:模拟多线程并发网络请求(I/O 密集型)
import time
from concurrent.futures import ThreadPoolExecutor
def fetch_webpage(url: str, delay: int) -> str:
print(f"开始请求: {url}")
# 模拟网络等待
time.sleep(delay)
print(f"请求完成: {url}")
return f"{url} 的网页数据"
def main():
urls = [
("http://example.com/page1", 2),
("http://example.com/page2", 1),
("http://example.com/page3", 3),
("http://example.com/page4", 1)
]
start_time = time.time()
# 创建一个最大容纳 4 个线程的线程池
with ThreadPoolExecutor(max_workers=4) as executor:
# 提交任务并获取 Future 对象
futures = [executor.submit(fetch_webpage, url, delay) for url, delay in urls]
# 获取返回结果
results = [fut.result() for fut in futures]
end_time = time.time()
print(f"线程池执行完毕。总共耗时: {end_time - start_time:.2f} 秒")
print(f"获取的数据条数: {len(results)}")
if __name__ == '__main__':
main()
运行分析:如果单线程顺序执行,总耗时将是 $2+1+3+1 = 7$ 秒。得益于多线程并发释放 GIL,多线程并行的总耗时仅取决于耗时最长的那个任务(即 3 秒)。
四、 多进程实战:Multiprocessing 与进程池
对于计算密集型任务,我们需要使用 multiprocessing 模块或 concurrent.futures.ProcessPoolExecutor。这会创建多个独立的 Python 解释器进程,每个进程拥有独立的 GIL,从而真正利用多核 CPU。
实战:大数值斐波那契数列计算(CPU 密集型)
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
def run_cpu_tasks(executor_class, max_workers, label):
nums = [35, 35, 35, 35] # 计算 4 个耗时较长的斐波那契数
start_time = time.time()
with executor_class(max_workers=max_workers) as executor:
results = list(executor.map(fibonacci, nums))
end_time = time.time()
print(f"[{label}] 耗时: {end_time - start_time:.2f} 秒,计算结果: {results}")
def main():
print("准备运行 CPU 密集型计算测试...")
# 1. 尝试使用多线程 (由于 GIL,无法多核加速)
run_cpu_tasks(ThreadPoolExecutor, 4, "多线程模式")
# 2. 尝试使用多进程 (真正多核并行)
run_cpu_tasks(ProcessPoolExecutor, 4, "多进程模式")
if __name__ == '__main__':
main()
运行分析: 在拥有多核 CPU 的现代电脑上运行此脚本,你会发现: * 多进程模式 的耗时远远短于 多线程模式(通常能缩短 3 到 4 倍,具体取决于核心数)。因为多进程拉起了 4 个独立的操作系统进程,分发给 4 个 CPU 核心同时跑,而多线程只能在一个核上通过时间片轮转切换执行。
五、 并发编程避坑指南
-
数据竞争与线程安全:多线程共享内存,因此当多个线程同时修改同一个全局变量时,会导致数据错乱。此时必须引入互斥锁
threading.Lock。 ```python import threading lock = threading.Lock()def safe_increment(): global counter with lock: # 自动获取和释放锁,保证只有一个线程执行此段代码 counter += 1
`` 2. **避免死锁 (Deadlock)**:多个进程或线程在互相等待对方释放资源时会产生死锁。设计并发程序时,应尽量减少嵌套锁的使用,或者使用超时锁。 3. **进程间通信 (IPC) 的成本**:由于进程间内存隔离,进程之间传递大数据对象(例如大型的 Pandas DataFrame 或 Numpy 数组)需要进行序列化和反序列化操作,这会带来额外的性能开销。 4. **注意if name == 'main':` 保护**:在 Windows 平台上使用多进程时,必须添加此保护入口,否则子进程会循环导入主模块导致递归报错。
通过本篇教程,相信你已经掌握了 Python 并发的核心原理。在开发智能体(Agent)系统时,由于大量的外部网络 API 请求(I/O 密集型),使用 ThreadPoolExecutor 或异步编程 asyncio 是极佳的选择;而在做本地数据清洗或离线特征工程时,则应该果断使用 ProcessPoolExecutor 释放多核威力。
本站所有文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。



暂无评论
还没有人评论过本文,快来发表你的高见吧!