在 Python 中,多线程和多进程是常用的并发编程方式。它们都可以提高程序的执行效率,但是它们的实现原理却有很大的不同。本文将深入探讨 Python 多线程和多进程的区别,并解释为什么多线程被称为 “假的多线程”。我也是在刚开始入门 python 的时候,处理数据的时候发现多线程的效率反而比单线程的低问题才进一步了解到这个的。
多线程和多进程的区别#
多线程和多进程都是并发编程的方式,但它们的实现方式却有很大的不同。多进程是指在操作系统中同时运行多个进程,每个进程都有自己独立的地址空间和系统资源。多线程是指在同一个进程中同时运行多个线程,每个线程共享同一个地址空间和系统资源。
具体来说,多进程是通过创建新的进程来实现并发的。每个进程都有自己独立的地址空间和系统资源,进程之间通过 IPC(Inter-Process Communication)机制来进行通信。多线程则是通过创建新的线程来实现并发的。所有线程共享同一个地址空间和系统资源,线程之间通过共享内存和同步机制来进行通信。
由于多进程是在操作系统层面实现的,并且每个进程都有自己独立的地址空间和系统资源,所以多进程具有更好的稳定性和安全性。但是创建和销毁进程需要消耗较大的系统资源,因此多进程适用于 CPU 密集型的任务。而多线程则适用于 IO 密集型的任务,因为线程之间切换的代价比进程之间切换的代价要小得多。
为什么多线程是假的多线程#
在 Python 中,由于 GIL(Global Interpreter Lock)机制的存在,多线程并不能真正地实现并发。GIL 是 Python 解释器中的一个锁,它保证同一时刻只有一个线程可以执行 Python 字节码。这意味着在任何时候只有一个线程可以真正地执行 Python 代码,即使在多核 CPU 上也是如此。
由于 GIL 机制的存在,Python 中的多线程被称为 “假的多线程”。虽然多个线程可以同时存在于内存中,但是它们并不能真正地并发执行 Python 代码。因此,在 Python 中使用多线程并不能提高 CPU 密集型任务的执行效率。
但是,对于 IO 密集型任务来说,Python 中的多线程还是有优势的。因为在 IO 密集型任务中,大部分时间都花费在等待 IO 操作完成上面。在这种情况下,GIL 机制并不会对程序的执行效率造成太大的影响。
多线程和多进程的开启方式#
在 Python 中,开启多线程或者多进程都有多种方式。下面分别介绍它们的优缺点。
多线程开启方式#
1. 使用 threading 模块#
import threading
def worker():
print("I'm working")
t = threading.Thread(target=worker)
t.start()
使用 threading 模块创建新线程非常简单,只需要创建一个 Thread 对象,并将要执行的函数作为参数传入即可。但是由于 GIL 机制的存在,使用多线程并不能真正地实现并发执行。
2. 使用 concurrent.futures 模块#
from concurrent.futures import ThreadPoolExecutor
def worker():
print("I'm working")
with ThreadPoolExecutor() as executor:
executor.submit(worker)
使用 concurrent.futures 模块可以更方便地创建新线程,并且还可以使用 ThreadPoolExecutor 类来管理线程池。但是由于 GIL 机制的存在,使用多线程并不能真正地实现并发执行。
多进程开启方式#
1. 使用 multiprocessing 模块#
import multiprocessing
def worker():
print("I'm working")
p = multiprocessing.Process(target=worker)
p.start()
使用 multiprocessing 模块创建新进程非常简单,只需要创建一个 Process 对象,并将要执行的函数作为参数传入即可。由于每个进程都有自己独立的地址空间和系统资源,因此可以真正地实现并发执行。
2. 使用 concurrent.futures 模块#
from concurrent.futures import ProcessPoolExecutor
def worker():
print("I'm working")
with ProcessPoolExecutor() as executor:
executor.submit(worker)
使用 concurrent.futures 模块可以更方便地创建新进程,并且还可以使用 ProcessPoolExecutor 类来管理进程池。由于每个进程都有自己独立的地址空间和系统资源,因此可以真正地实现并发执行。
代码样例#
下面给出一个使用 multiprocessing 模块实现并行计算圆周率的例子:
import multiprocessing
import time
def calc_pi(digits):
start = time.time()
pi = 0
for k in range(digits):
pi += ((-1) ** k) / (2 * k + 1)
pi *= 4
end = time.time()
print(f"Digits: {digits}, Time: {end - start:.4f}s")
return pi
if __name__ == '__main__':
with multiprocessing.Pool() as pool:
digits_list = [100000, 200000, 300000, 400000, 500000]
results = pool.map(calc_pi, digits_list)
for digits, pi in zip(digits_list, results):
print(f"Digits: {digits}, Pi: {pi:.10f}")
在这个例子中,我们使用 multiprocessing.Pool 类创建了一个进程池,并使用 map () 方法对 digits_list 中的每个元素调用 calc_pi () 函数进行计算。由于每个元素都在不同的进程中进行计算,因此可以真正地实现并行计算圆周率。
总结#
本文深入探讨了 Python 多线程和多进程的区别,并解释了为什么多线程被称为 “假的多线程”。同时,我们还介绍了各种开启多线程或者多进程的方法,并给出了代码样例。在实际开发中,应该根据任务类型选择合适的并发编程方式,以提高程序的执行效率。
20230712 更新#
据今天的新闻说 Meta 公司承诺要花费三个工程师年 来删除 GIL,现在在等待 Python 社区接受 PEP703 的提案。source