可能是 Python 线程、进程和 GIL 的最简单教程

原文:towardsdatascience.com/dont-know-what-is-python-gil-this-may-be-the-easiest-tutorial-3b99805d2225

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/6d36d0199643bb6e6b317a417ec341d7.png

图片由 Regina 提供,来自 Pixabay

如果你是一名 Python 学习者,请不要害怕,因为这篇文章旨在用最简单的方式向你解释什么是 GIL。当然,我们必须从解释线程和进程开始。不用担心,我会尽力让每个人都能理解,尽管这可能会牺牲一些定义的准确性。

现在我们应该开始了。

1. Python 中的多线程

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/882227441de7b5d2a4ca287630d687e5.png

图片由 Steen Jepsen 提供,来自 Pixabay

一些概念

多线程是最常见的编程技术之一,Python 中也存在这种技术。

它允许我们同时运行多个操作。通常,多线程可以提高 CPU 的使用效率。此外,大多数 I/O 任务都可以从并发运行的线程中受益。

请不要对概念“进程”和“线程”感到困惑。进程将分配一定的内存,并且在操作系统中与其他进程完全隔离。因此,在我们的操作系统中,一个程序崩溃通常不会影响其他程序。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7581ccd66d78547ddf5cf61e0c3b5d98.png

进程和线程之间的关系

一个进程可能在其下运行多个线程,共享许多相同的资源,如内存。因此,一个线程崩溃将导致整个进程崩溃。因为线程之间共享内存,这也可能在进程中引起麻烦。我稍后会演示。

代码示例

现在,让我们看看如何使用多线程技术编写 Python 代码。

首先,让我们导入 Python 内置的threading模块。

import threading

为了能够测试多线程,让我们定义一个足够简单但需要一些时间的函数。

def compute():
    for _ in range(100000000):
        pass

这个函数什么都不做,它只是执行一个循环 100 亿次,每次循环不进行任何计算。

然后,假设我们想要在两个不同的线程中运行compute()函数两次。我们可以在 Python 中使用threading.Thread()创建一个新的线程。之后,target=compute告诉线程执行compute()函数。

threading.Thread(target=compute)

现在,让我们创建两个线程t1t2,然后要求它们执行compute()函数。

t1 = threading.Thread(target=compute)
t2 = threading.Thread(target=compute)
t1.start()
t2.start()
t1.join()
t2.join()

在上面的代码中,t1.start()将告诉线程开始执行分配给它的任何任务。在我们的例子中,它是执行compute()函数。同样,我们通过运行代码t2.start()立即让t2开始。

当你看到代码t1.join()时,这意味着我们希望进程等待线程t1完成。换句话说,我们希望在上面的代码中等待t1t2都完成它们的工作。

2. Python 中的 GIL 是什么?

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/aa145ee21466aa2d9b781f2578647bfd.png

图片由Manfred Richter提供,来自Pixabay

GIL 代表全局解释器锁。这就是你需要了解的文本概念。要理解它,请查看下面的图示。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c8bcfc9f95ae00e63f3f6fd316b823fc.png

Python 中 GIL 的工作原理

如上图所示,GIL 只允许一次只有一个线程运行。当一个线程请求开始工作时,它会锁定其他线程。

因此,尽管我们让线程 1 和线程 2 同时运行,但它们根本无法利用多个 CPU 核心,因此对性能的提升几乎没有帮助。事实上,大多数情况下可能会更糟。

为什么 Python 有这样一个机制?以下是一些原因。

线程安全

由于多线程共享相同的内存和 I/O,可能会出现“线程竞态”问题。以下是一个典型的例子。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/25a3cdb9d2ac91693e4e25f7bd56e895.png

线程不安全的典型示例

在开始时,两种情况下x=1都是相同的,并且值已经在内存中。然后,由于两个线程是并行运行的,我们不知道哪个先运行。最终,x的结果将变得神秘。这是线程不安全的一个典型例子。

历史问题

在 Python 刚出现的时候,还没有出现多核 CPU 的通用 CPU。因此,拥有 GIL 的好处绝对超过了它的限制。

例如,CPython 中的内存管理将会简单得多,这使得垃圾回收过程变得直接。同时,它避免了由线程不安全引起的许多复杂错误,例如竞态条件。

展示了限制

现在,随着 CPU 技术的发展和 Python 在数据分析/科学/工程领域的广泛应用,GIL 已经成为 Python 性能和灵活性的主要瓶颈。

然而,一些学习者可能会争论说多进程可以是多线程的替代品。在下一节中,我将尝试解释什么是多进程以及为什么它不同。

3. 为什么多进程不同?

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0829b7c17c9ddfbd66847c2d568abdbd.png

图片由 Lucent_Designs_dinoson20 来自 Pixabay 提供

再次强调,我不喜欢写很多文字来讲述枯燥的知识。请参见以下图表。它展示了单线程、多线程和多进程之间的差异。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/650a30d5e2481b35b28aebdca676ee1f.png

多线程和多进程之间的差异

在广义上,使用 GIL 的多线程可以完成与单线程相似数量的任务。然而,多进程可以绕过多线程的限制。在同一时间段内,它可能能够完成比其他两种方法多一倍的任务量。

证明理论

让我们开始编码!

由于在交互式环境(如 Jupyter Notebook)中使用多进程并不容易,让我们编写一个最简单的 Python 脚本文件来比较这三种场景:

  • 单线程

  • 多线程

  • 单线程的多进程

首先,我们需要导入相关模块。然后,让我们使用上面用过的相同函数 compute()。为了方便起见,我再次粘贴代码在这里。

from threading import Thread
from multiprocessing import Process
import time

def compute():
    for _ in range(100000000):
        pass

单线程的代码非常简单,只需运行函数两次即可,因为在其他两个例子中,我们需要将它们分别放入两个不同的线程和进程中。

# Single Threads
start = time.time()

compute()
compute()

end = time.time()
print("Time taken with single threads:", end - start)

多线程的代码与上一个例子相同。为了方便起见,我再次粘贴代码在这里。

# Multi-Threading
start = time.time()

t1 = Thread(target=compute)
t2 = Thread(target=compute)
t1.start()
t2.start()
t1.join()
t2.join()

end = time.time()
print("Time taken with multi-threads:", end - start)

多进程的代码与多线程的代码基本相同。唯一的区别是使用 Process 工厂类。

# Multi-Processing
start = time.time()

p1 = Process(target=compute)
p2 = Process(target=compute)
p1.start()
p2.start()
p1.join()
p2.join()

end = time.time()
print("Time taken with multi-process:", end - start)

然后,让我们将所有上述代码放入一个名为 my_test.py 的脚本文件中。之后,我们可以通过运行 Python 脚本文件来测试这些场景。

$ python my_test.py

这里是结果。它显示结果大致符合上述图表中展示的内容。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7648ff0be40720e953aac124c34f019c.png

如果你仍然不理解发生了什么,我创建了一个如下所示的另一个图表。它说明了为什么多进程可以减少耗时。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3acd427f1b571b91abc864f05329ecdf.png

为什么多进程比 GIL 启用的多线程更快?

BTW,由于我的计算操作系统在测试进行时有大量进程运行,结果可能不准确,但它应该能在高层次上证明理论。

为什么多进程不是理想的选择?

如前图所示,多进程不共享内存分配、I/O 资源等。这就是为什么 GIL 对它没有影响。然而,这也是它不够灵活的原因。

实际情况可能更加复杂。尽管进程间通信仍然是可能的,但它会带来很多开销。有时,我们从多进程中获得的性能提升可能会被耗尽。

回到多线程,多个线程可以轻松地在同一内存位置上工作,并通过并行利用硬件资源共同完成大任务。当然,避免复杂线程相关错误的责任将落在开发者身上。

4. 这将得到解决!

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/78c37055453bf18de6927454d15ad367.png

图片由 Gabriele LässerPixabay 提供

最后,我提出引入 GIL 的想法是因为它有望很快被移除。使 CPython 中的 GIL 可选的 PEP 703 已被接受

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8e5115b18f4e30a4c12aa5d972477ca4.png

来自公共网站的截图:peps.python.org/pep-0703/

PEP 703 – 在 CPython 中使全局解释器锁可选

摘要

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/550b3f6751390f35ac1ee6cd6e1a932a.png

图片由 TImorPixabay 提供

在这篇文章中,我介绍了 Python 中最独特的机制之一——全局解释器锁(GIL)。它是一把双刃剑。虽然它简化了内存管理并避免了某些线程不安全的情况,但它也限制了开发者充分利用现代硬件(如多核 CPU)来更有效地处理任务的能力。

希望这篇文章能帮助你理解线程和进程是什么。PEP 703 的接受也让我们有理由相信 Python 的灵活性在未来会更好。

除非另有说明,所有图片均由作者提供

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值