Day05: Python 中的并发和并行(1)
理解 Python 中的线程和进程
理解线程和进程是实现在 Python 中并发和并行的基础。这种知识使你能够编写能够看似同时执行多个任务的程序,从而提高性能和响应能力。本课程将深入探讨线程和进程的核心概念、它们的区别,以及它们如何为更高级的并发技术奠定基础。
线程:轻量级并发
线程是同一进程内的独立执行路径。它们共享相同的内存空间,这使得它们能够轻松访问和修改相同的数据。这种共享内存模型使得线程间的通信相对简单,但也引入了竞态条件和其它同步问题的风险。
线程的概念
线程可以被视为一个轻量级子进程。操作系统管理线程,允许它们并发运行。当一个线程正在等待资源或执行阻塞操作(如 I/O)时,操作系统可以切换到另一个线程,使 CPU 保持忙碌。
示例: 想象一个文字处理器。一个线程可能处理用户输入,另一个线程可能在后台执行拼写检查,还有一个线程可能定期自动保存文档。所有这些线程都在同一个文字处理器应用程序(进程)中运行。
线程的优点
- 轻量级: 创建和销毁线程通常比创建和销毁进程更快,并且消耗更少的资源。
- 共享内存: 同一进程内的线程可以轻松共享数据,简化了通信和数据交换。
- 响应性: 多线程可以通过允许应用程序在某个线程被阻塞时继续执行其他任务来提高其响应性。
线程的缺点
- 全局解释器锁 (GIL): 在 CPython 中,标准的 Python 解释器,GIL 允许在任何给定时间只有一个线程持有对 Python 解释器的控制权。这限制了多线程 Python 程序中 CPU 密集型任务的真实并行性。然而,I/O 密集型任务仍然可以从多线程中受益,因为当线程等待 I/O 时 GIL 会被释放。
- 同步问题: 由于线程共享内存,如果不同步,它们可能会相互干扰。这可能导致竞态条件、死锁和其他并发问题。
- 调试复杂性: 由于线程执行的不可确定性,调试多线程程序可能比调试单线程程序更具挑战性。
线程的实际示例
- Web Server: Web 服务器可以使用多个线程来并发处理传入的客户端请求。每个线程可以独立地处理一个请求,使服务器能够同时服务多个客户端。
- 图形界面应用程序: 图形界面应用程序通常使用线程来保持用户界面的响应性。例如,一个单独的线程可以处理长时间运行的任务,如下载文件,而不会阻塞主 UI 线程。
- 数据处理: 数据处理应用程序可以使用线程来并发处理不同的数据块,从而加快整体处理时间。
代码示例:基本线程
import threading
import timedef task(name):print(f"Thread {name}: Starting")time.sleep(2) # Simulate a time-consuming taskprint(f"Thread {name}: Finishing")# 创建并启动多个线程
threads = []
for i in range(3):t = threading.Thread(target=task, args=(f"Thread-{i+1}",))threads.append(t)t.start()# 等待所有线程完成
for t in threads:t.join()print("所有线程完成")
解释:
threading
模块被导入以处理线程。task
函数使用time.sleep()
模拟耗时操作。- 创建了三个线程,每个线程以不同的名称运行
task
函数。 t.start()
启动每个线程。t.join()
会等待每个线程完成后再继续主程序。
进程:独立执行
进程是并发运行的独立程序。每个进程都有自己独立的内存空间,这意味着进程不能直接共享数据。进程之间的通信需要进程间通信(IPC)机制,例如管道、套接字或共享内存。
进程的概念
进程是一个正在执行的程序的实例。操作系统为每个进程分配资源,例如内存和 CPU 时间。进程之间是相互隔离的,这意味着一个进程不能直接访问另一个进程的内存或资源。
示例: 考虑运行两个文本编辑器实例。每个实例是一个独立进程,拥有自己的内存空间和资源。如果一个实例崩溃,它不会影响另一个实例。
进程的优势
- 真正的并行性: 进程可以在多核处理器上实现真正的并行性,因为每个进程都有自己的解释器和内存空间,从而绕过了 GIL 的限制。
- 隔离: 进程彼此隔离,这意味着一个进程的崩溃不会影响其他进程。
- 安全: 进程隔离通过防止一个进程访问或修改其他进程的数据来增强安全性。
进程的缺点
- 开销: 创建和销毁进程通常比创建和销毁线程更耗费资源。
- 进程间通信(IPC): 进程间的通信需要 IPC 机制,这比线程间的共享内存通信可能更复杂且更慢。
- 内存占用: 每个进程都有自己的内存空间,与多线程应用程序相比,这可能导致更大的内存占用。
进程的实际案例
- 视频编码: 一个视频编码应用程序可以使用多个进程同时编码视频的不同片段,显著减少编码时间。
- 科学计算: 科学计算应用通常使用多个进程来并行执行复杂的计算,利用多核处理器的强大功能。
- 分布式系统: 分布式系统使用运行在不同机器上的多个进程来并行执行任务,并提供高可用性和可扩展性。
代码示例:基本多进程
import multiprocessing
import timedef task(name):print(f"Process {name}: Starting")time.sleep(2) # 模拟耗时的任务print(f"Process {name}: Finishing")if __name__ == "__main__":# 创建并启动多个进程processes = []for i in range(3):p = multiprocessing.Process(target=task, args=(f"Process-{i+1}",))processes.append(p)p.start()# 等待所有进程完成for p in processes:p.join()print("所有进程完成")
解释:
multiprocessing
模块被导入以处理进程。task
函数使用time.sleep()
模拟耗时操作。- 创建了三个进程,每个进程都运行不同的
task
函数。 p.start()
启动每个进程。p.join()
会等待每个进程完成后再继续主程序。if __name__ == "__main__":
代码块是必要的,以防止在某些操作系统上子进程递归地创建新进程。
线程与进程:主要区别
特性 | 线程 | 进程 |
---|---|---|
内存空间 | 共享 | 独立 |
资源消耗 | 轻量级 | 重量级 |
并行性 | 受 GIL(在 CPython 中)限制 | 多核 CPU 上的真正并行 |
隔离 | 低 | 高 |
通信 | 共享内存,更容易实现 | IPC 机制,更复杂 |
上下文切换 | 更快 | 更慢 |
碰撞影响 | 一个线程崩溃会影响整个进程 | 一个进程崩溃不会影响其他进程 |
在线程和进程之间进行选择
- I/O 密集型任务: 对于 I/O 密集型任务,线程通常是较好的选择,因为程序大部分时间都在等待 I/O 操作完成。当线程等待 I/O 时,GIL 会被释放,允许其他线程运行。
- CPU 密集型任务: 对于 CPU 密集型任务,进程通常是更好的选择,因为程序大部分时间都在进行计算。进程可以在多核处理器上实现真正的并行性,绕过 GIL 的限制。
- 隔离: 如果隔离是一个关键要求,进程是首选的选择。
- 复杂性: 线程通常比进程更容易实现和管理,尤其是在共享数据方面。
实际应用
考虑一个大规模数据处理流程。该流程包含多个阶段,如数据采集、数据清洗、数据转换和数据分析。线程和进程都可以用来并行化该流程,但选择取决于所涉及任务的性质。
- 数据采集: 如果数据采集涉及从多个网络源读取数据(I/O 密集型),可以使用线程来同时处理多个连接。
- 数据清洗和转换: 如果数据清洗和转换涉及 CPU 密集型操作,例如复杂的计算或字符串操作,可以使用进程来利用多个核心,实现真正的并行处理。
- 数据分析: 根据分析的复杂性,可以使用线程或进程。对于简单的分析任务,线程可能就足够了。对于更复杂的数据分析任务,可能需要使用进程来达到最佳性能。