前面我们学习了多进程和多线程的使用,这二者都可以用来实现并发编程,那么具体应该在什么场景下使用多进程,什么场景下使用多线程呢?
首先我们需要明确一点,多进程和多线程都可以用来实现并发编程,但是多进程更适合 CPU 密集型任务,而多线程更适合 IO 密集型任务。这是为什么呢?
首先我们来看看多进程,多进程是由操作系统来调度的,每个进程有自己独立的内存空间,所以多进程可以充分利用多核 CPU 的优势,每个进程都可以同时运行在不同的 CPU 核上,这样就可以实现并行执行,当然代价是进程间的通信和数据共享比线程更复杂和开销更大。
而多个线程共享同一块内存空间,所以多线程可以共享全局变量,这样就可以实现线程之间的通信。在进行 I/O 密集型任务时,例如网络请求或文件读写,多线程可以在等待 I/O 完成时执行其他线程的任务。所以多线程更适合 I/O 密集型任务。
这里还有一个需要重点说明的是 Python 中还有一个奇怪的设计,就是 GIL(Global Interpreter Lock,全局解释器锁),GIL 是 Python 解释器(CPython)设计的一个锁,CPython 在执行任何代码时,都需要对应的线程先获得 GIL,然后每执行 100 条(字节码)指令,CPython 就会让获得 GIL 的线程主动释放 GIL,这样别的线程才有机会执行。因为 GIL 的存在,无论你的 CPU 有多少个核,我们编写的 Python 代码也没有机会真正并行的执行。它的主要目的是简化 Python 解释器的实现,确保线程安全,但同时也带来了性能上的影响。所以多线程实际上在 Python 中并不能真正的并行执行,只能是并发执行。
那么对于多进程和多线程 GIL 有什么影响呢?
对多线程的影响:
- CPU 密集型任务:GIL 限制了 Python 在多线程环境下对 CPU 密集型任务的并行处理能力。由于 GIL 确保同一时刻只有一个线程可以执行 Python 字节码,多线程在执行计算密集型任务时,无法有效利用多核 CPU 的计算能力。
- I/O 密集型任务:对于 I/O 密集型任务(如文件读写、网络操作等),GIL 的影响较小。在这种情况下,线程在等待 I/O 操作完成时被阻塞,会主动释放 GIL,这时其他线程可以继续执行,因此多线程在 I/O 密集型任务中仍然能够发挥作用。
对多进程的影响:
多进程模型不会受到 GIL 的影响,因为每个进程都有自己独立的 Python 解释器和内存空间。进程之间的通信和数据共享是通过进程间通信机制(如管道、队列等)实现的,不受 GIL 限制。因此,多进程能够充分利用多核 CPU 的计算能力,适用于 CPU 密集型任务。
GIL 是官方 Python 解释器(CPython)在设计上的历史遗留问题,我们可以使用其他的 Python 解释器来避免 GIL 的影响,比如 Jython、Pypy、IronPython 等,这些解释器都没有 GIL 的限制,但是这些解释器有一些局限性,比如有些模块无法使用,性能可能不如 CPython 等。
要在 CPython 中去解决这个问题,让多线程能够发挥 CPU 的多核优势,需要重新实现一个不带 GIL 的 Python 解释器,按照官方的说法,在 Python 发布 4.0 版本时会得到解决,当然这个改动可能会带来一些不兼容的问题,所以让我们拭目以待吧。